diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce41cf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vendor/ +var/ +.idea/ +.phpunit.result.cache \ No newline at end of file diff --git a/bin/doctrine.php b/bin/doctrine.php new file mode 100755 index 0000000..97833c7 --- /dev/null +++ b/bin/doctrine.php @@ -0,0 +1,12 @@ +#!/usr/bin/env php +get(\Doctrine\ORM\EntityManagerInterface::class)) +); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b01c5fb --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "lubiana/saufen", + "type": "project", + "require": { + "php": "^8.4", + "php-di/slim-bridge": "^3.4", + "doctrine/orm": "^3.3", + "monolog/monolog": "^3.5", + "slim/twig-view": "^3.4", + "slim/http": "^1.4", + "slim/psr7": "^1.7", + "ext-pdo": "*", + "symfony/cache": "^7.3" + }, + "require-dev": { + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan": "^2.1", + "rector/rector": "^2.0", + "symplify/easy-coding-standard": "^12.5", + "pestphp/pest": "^3.8", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan-doctrine": "^2.0" + }, + "scripts": { + "lint": [ + "rector", + "ecs --fix || ecs --fix" + ], + "test": "pest --parallel" + }, + "license": "MIT", + "autoload": { + "psr-4": { + "App\\": "src/", + "Tests\\": "tests/" + } + }, + "authors": [ + { + "name": "lubiana", + "email": "lubiana@hannover.ccc.de" + } + ], + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..aa8c349 --- /dev/null +++ b/composer.lock @@ -0,0 +1,6543 @@ +{ + "_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": "4abcc9545d4156c4b0669813613244f7", + "packages": [ + { + "name": "doctrine/collections", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/2eb07e5953eed811ce1b309a7478a3b236f2273d", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "ext-json": "*", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.3.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2025-03-22T10:17:19+00:00" + }, + { + "name": "doctrine/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": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "doctrine/orm", + "version": "3.3.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "1f1891d3e20ef9881e81c2f32c53e9dc88dfc9a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/1f1891d3e20ef9881e81c2f32c53e9dc88dfc9a7", + "reference": "1f1891d3e20ef9881e81c2f32c53e9dc88dfc9a7", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/collections": "^2.2", + "doctrine/dbal": "^3.8.2 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^3", + "doctrine/persistence": "^3.3.1 || ^4", + "ext-ctype": "*", + "php": "^8.1", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.3.9 || ^7.0" + }, + "require-dev": { + "doctrine/coding-standard": "^13.0", + "phpbench/phpbench": "^1.0", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "2.0.3", + "phpstan/phpstan-deprecation-rules": "^2", + "phpunit/phpunit": "^10.4.0", + "psr/log": "^1 || ^2 || ^3", + "squizlabs/php_codesniffer": "3.12.0", + "symfony/cache": "^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/3.3.3" + }, + "time": "2025-05-02T17:42:51+00:00" + }, + { + "name": "doctrine/persistence", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "45004aca79189474f113cbe3a53847c2115a55fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/45004aca79189474f113cbe3a53847c2115a55fa", + "reference": "45004aca79189474f113cbe3a53847c2115a55fa", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1 || ^2", + "php": "^8.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "doctrine/common": "<2.10" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "1.12.7", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^9.6", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src/Persistence" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/4.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2024-11-01T21:49:07+00:00" + }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2025-03-19T13:51:03+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+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": "php-di/invoker", + "version": "2.3.6", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "59f15608528d8a8838d69b422a919fd6b16aa576" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/59f15608528d8a8838d69b422a919fd6b16aa576", + "reference": "59f15608528d8a8838d69b422a919fd6b16aa576", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "psr/container": "^1.0|^2.0" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "support": { + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.6" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2025-01-17T12:49:27+00:00" + }, + { + "name": "php-di/php-di", + "version": "7.0.10", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "0d1ed64126577e9a095b3204dcaee58cf76432c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/0d1ed64126577e9a095b3204dcaee58cf76432c2", + "reference": "0d1ed64126577e9a095b3204dcaee58cf76432c2", + "shasum": "" + }, + "require": { + "laravel/serializable-closure": "^1.0 || ^2.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" + }, + "suggest": { + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "DI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], + "support": { + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.10" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2025-04-22T08:53:15+00:00" + }, + { + "name": "php-di/slim-bridge", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Slim-Bridge.git", + "reference": "02ab0274a19d104d74561164f8915b62d93f3cf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Slim-Bridge/zipball/02ab0274a19d104d74561164f8915b62d93f3cf0", + "reference": "02ab0274a19d104d74561164f8915b62d93f3cf0", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-di/invoker": "^2.0.0", + "php-di/php-di": "^6.0|^7.0", + "slim/slim": "^4.2.0" + }, + "require-dev": { + "laminas/laminas-diactoros": "^2.1", + "mnapoli/hard-mode": "^0.3.0", + "phpunit/phpunit": ">= 7.0 < 10" + }, + "type": "library", + "autoload": { + "psr-4": { + "DI\\Bridge\\Slim\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP-DI integration in Slim", + "support": { + "issues": "https://github.com/PHP-DI/Slim-Bridge/issues", + "source": "https://github.com/PHP-DI/Slim-Bridge/tree/3.4.1" + }, + "time": "2024-06-19T15:47:45+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/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+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": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "slim/http", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim-Http.git", + "reference": "a8def7b8e9eabd0cdc21654ad4a82606942e066a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim-Http/zipball/a8def7b8e9eabd0cdc21654ad4a82606942e066a", + "reference": "a8def7b8e9eabd0cdc21654ad4a82606942e066a", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "php": "^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.4", + "doctrine/instantiator": "^1.3.1", + "laminas/laminas-diactoros": "^3.1.0", + "nyholm/psr7": "^1.8.1", + "php-http/psr7-integration-tests": "^1.3.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "^3.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + } + ], + "description": "Slim PSR-7 Object Decorators", + "homepage": "http://slimframework.com", + "keywords": [ + "http", + "psr-7", + "psr7" + ], + "support": { + "issues": "https://github.com/slimphp/Slim-Http/issues", + "source": "https://github.com/slimphp/Slim-Http/tree/1.4.0" + }, + "time": "2024-06-24T18:27:41+00:00" + }, + { + "name": "slim/psr7", + "version": "1.7.1", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim-Psr7.git", + "reference": "fe98653e7983010aa85c1d137c9b9ad5a1cd187d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/fe98653e7983010aa85c1d137c9b9ad5a1cd187d", + "reference": "fe98653e7983010aa85c1d137c9b9ad5a1cd187d", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1.5", + "php": "^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.0 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/polyfill-php80": "^1.29" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.0 || ^2.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.4", + "ext-json": "*", + "http-interop/http-factory-tests": "^1.0 || ^2.0", + "php-http/psr7-integration-tests": "^1.4", + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.2", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.6 || ^10", + "squizlabs/php_codesniffer": "^3.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Psr7\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "https://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "https://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "https://www.lgse.com" + } + ], + "description": "Strict PSR-7 implementation", + "homepage": "https://www.slimframework.com", + "keywords": [ + "http", + "psr-7", + "psr7" + ], + "support": { + "issues": "https://github.com/slimphp/Slim-Psr7/issues", + "source": "https://github.com/slimphp/Slim-Psr7/tree/1.7.1" + }, + "time": "2025-05-13T14:24:12+00:00" + }, + { + "name": "slim/slim", + "version": "4.14.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "5943393b88716eb9e82c4161caa956af63423913" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/5943393b88716eb9e82c4161caa956af63423913", + "reference": "5943393b88716eb9e82c4161caa956af63423913", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.4", + "ext-simplexml": "*", + "guzzlehttp/psr7": "^2.6", + "httpsoft/http-message": "^1.1", + "httpsoft/http-server-request": "^1.1", + "laminas/laminas-diactoros": "^2.17 || ^3", + "nyholm/psr7": "^1.8", + "nyholm/psr7-server": "^1.1", + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.1", + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^9.6", + "slim/http": "^1.3", + "slim/psr7": "^1.6", + "squizlabs/php_codesniffer": "^3.10", + "vimeo/psalm": "^5.24" + }, + "suggest": { + "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", + "ext-xml": "Needed to support XML format in BodyParsingMiddleware", + "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim", + "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://www.slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "support": { + "docs": "https://www.slimframework.com/docs/v4/", + "forum": "https://discourse.slimframework.com/", + "irc": "irc://irc.freenode.net:6667/slimphp", + "issues": "https://github.com/slimphp/Slim/issues", + "rss": "https://www.slimframework.com/blog/feed.rss", + "slack": "https://slimphp.slack.com/", + "source": "https://github.com/slimphp/Slim", + "wiki": "https://github.com/slimphp/Slim/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/slimphp", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slim/slim", + "type": "tidelift" + } + ], + "time": "2024-06-13T08:54:48+00:00" + }, + { + "name": "slim/twig-view", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Twig-View.git", + "reference": "b4268d87d0e327feba5f88d32031e9123655b909" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Twig-View/zipball/b4268d87d0e327feba5f88d32031e9123655b909", + "reference": "b4268d87d0e327feba5f88d32031e9123655b909", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/http-message": "^1.1 || ^2.0", + "slim/slim": "^4.12", + "symfony/polyfill-php81": "^1.29", + "twig/twig": "^3.11" + }, + "require-dev": { + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/phpstan": "^1.10.59", + "phpunit/phpunit": "^9.6 || ^10", + "psr/http-factory": "^1.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Views\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + } + ], + "description": "Slim Framework 4 view helper built on top of the Twig 3 templating component", + "homepage": "https://www.slimframework.com", + "keywords": [ + "framework", + "slim", + "template", + "twig", + "view" + ], + "support": { + "issues": "https://github.com/slimphp/Twig-View/issues", + "source": "https://github.com/slimphp/Twig-View/tree/3.4.1" + }, + "time": "2024-09-26T05:42:02+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "c4b217b578c11ec764867aa0c73e602c602965de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/c4b217b578c11ec764867aa0c73e602c602965de", + "reference": "c4b217b578c11ec764867aa0c73e602c602965de", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-06T19:00:13+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T15:25:07+00:00" + }, + { + "name": "symfony/console", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-24T10:34:04+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.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-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.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.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "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 intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "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.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "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.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "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\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "000df7860439609837bbe28670b0be15783b7fbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", + "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-20T12:04:08+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-20T20:19:01+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "c9a1168891b5aaadfd6332ef44393330b3498c4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/c9a1168891b5aaadfd6332ef44393330b3498c4c", + "reference": "c9a1168891b5aaadfd6332ef44393330b3498c4c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-15T09:04:05+00:00" + }, + { + "name": "twig/twig", + "version": "v3.21.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.21.1" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-05-03T07:21:55+00:00" + } + ], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.8.3", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "a585c346ddf1bec22e51e20b5387607905604a71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/a585c346ddf1bec22e51e20b5387607905604a71", + "reference": "a585c346ddf1bec22e51e20b5387607905604a71", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.2.0", + "jean85/pretty-package-versions": "^2.1.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "phpunit/php-code-coverage": "^11.0.9 || ^12.0.4", + "phpunit/php-file-iterator": "^5.1.0 || ^6", + "phpunit/php-timer": "^7.0.1 || ^8", + "phpunit/phpunit": "^11.5.11 || ^12.0.6", + "sebastian/environment": "^7.2.0 || ^8", + "symfony/console": "^6.4.17 || ^7.2.1", + "symfony/process": "^6.4.19 || ^7.2.4" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.6", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2.0.3", + "squizlabs/php_codesniffer": "^3.11.3", + "symfony/filesystem": "^6.4.13 || ^7.2.0" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.8.3" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2025-03-05T08:29:11+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.0", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", + "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.0" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-03-15T12:00:00+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-04-29T12:36:36+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.8.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/4cf9f3b47afff38b139fb79ce54fc71799022ce8", + "reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.0", + "nunomaduro/termwind": "^2.3.0", + "php": "^8.2.0", + "symfony/console": "^7.2.5" + }, + "conflict": { + "laravel/framework": "<11.44.2 || >=13.0.0", + "phpunit/phpunit": "<11.5.15 || >=13.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.3", + "larastan/larastan": "^3.2", + "laravel/framework": "^11.44.2 || ^12.6", + "laravel/pint": "^1.21.2", + "laravel/sail": "^1.41.0", + "laravel/sanctum": "^4.0.8", + "laravel/tinker": "^2.10.1", + "orchestra/testbench-core": "^9.12.0 || ^10.1", + "pestphp/pest": "^3.8.0", + "sebastian/environment": "^7.2.0 || ^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-04-03T14:33:09+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.2.6" + }, + "require-dev": { + "illuminate/console": "^11.44.7", + "laravel/pint": "^1.22.0", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.2", + "phpstan/phpstan": "^1.12.25", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.2.6", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2025-05-08T08:14:37+00:00" + }, + { + "name": "pestphp/pest", + "version": "v3.8.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "c6244a8712968dbac88eb998e7ff3b5caa556b0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/c6244a8712968dbac88eb998e7ff3b5caa556b0d", + "reference": "c6244a8712968dbac88eb998e7ff3b5caa556b0d", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.8.3", + "nunomaduro/collision": "^8.8.0", + "nunomaduro/termwind": "^2.3.0", + "pestphp/pest-plugin": "^3.0.0", + "pestphp/pest-plugin-arch": "^3.1.0", + "pestphp/pest-plugin-mutate": "^3.0.5", + "php": "^8.2.0", + "phpunit/phpunit": "^11.5.15" + }, + "conflict": { + "filp/whoops": "<2.16.0", + "phpunit/phpunit": ">11.5.15", + "sebastian/exporter": "<6.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "pestphp/pest-dev-tools": "^3.4.0", + "pestphp/pest-plugin-type-coverage": "^3.5.0", + "symfony/process": "^7.2.5" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v3.8.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-04-17T10:53:02+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/e79b26c65bc11c41093b10150c1341cc5cdbea83", + "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.2" + }, + "conflict": { + "pestphp/pest": "<3.0.0" + }, + "require-dev": { + "composer/composer": "^2.7.9", + "pestphp/pest": "^3.0.0", + "pestphp/pest-dev-tools": "^3.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", + "keywords": [ + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin/tree/v3.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2024-09-08T23:21:41+00:00" + }, + { + "name": "pestphp/pest-plugin-arch", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/db7bd9cb1612b223e16618d85475c6f63b9c8daa", + "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^3.0.0", + "php": "^8.2", + "ta-tikoma/phpunit-architecture-test": "^0.8.4" + }, + "require-dev": { + "pestphp/pest": "^3.8.1", + "pestphp/pest-dev-tools": "^3.4.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Arch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Arch plugin for Pest PHP.", + "keywords": [ + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-04-16T22:59:48+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v3.0.5", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/e10dbdc98c9e2f3890095b4fe2144f63a5717e08", + "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.2.0", + "pestphp/pest-plugin": "^3.0.0", + "php": "^8.2", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "pestphp/pest": "^3.0.8", + "pestphp/pest-dev-tools": "^3.0.0", + "pestphp/pest-plugin-type-coverage": "^3.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v3.0.5" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2024-09-22T07:54:40+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" + }, + "time": "2025-04-13T19:20:35+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + }, + "time": "2025-02-19T13:28:12+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.17", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-05-21T20:55:28+00:00" + }, + { + "name": "phpstan/phpstan-doctrine", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-doctrine.git", + "reference": "4497663eb17b9d29211830df5aceaa3a4d256a35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/4497663eb17b9d29211830df5aceaa3a4d256a35", + "reference": "4497663eb17b9d29211830df5aceaa3a4d256a35", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.13" + }, + "conflict": { + "doctrine/collections": "<1.0", + "doctrine/common": "<2.7", + "doctrine/mongodb-odm": "<1.2", + "doctrine/orm": "<2.5", + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "cache/array-adapter": "^1.1", + "composer/semver": "^3.3.2", + "cweagans/composer-patches": "^1.7.3", + "doctrine/annotations": "^2.0", + "doctrine/collections": "^1.6 || ^2.1", + "doctrine/common": "^2.7 || ^3.0", + "doctrine/dbal": "^3.3.8", + "doctrine/lexer": "^2.0 || ^3.0", + "doctrine/mongodb-odm": "^2.4.3", + "doctrine/orm": "^2.16.0", + "doctrine/persistence": "^2.2.1 || ^3.2", + "gedmo/doctrine-extensions": "^3.8", + "nesbot/carbon": "^2.49", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6.20", + "ramsey/uuid": "^4.2", + "symfony/cache": "^5.4" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Doctrine extensions for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-doctrine/issues", + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.3" + }, + "time": "2025-05-05T15:28:52+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.4" + }, + "time": "2025-03-18T11:42:40+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.4.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.2" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-25T13:26:39+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-27T05:02:59+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.15", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", + "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.9", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.1", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.3.0", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.2", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.15" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-03-23T16:02:11+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "rector/rector", + "version": "2.0.16", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "f1366d1f8c7490541c8f7af6e5c7cef7cca1b5a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/f1366d1f8c7490541c8f7af6e5c7cef7cca1b5a2", + "reference": "f1366d1f8c7490541c8f7af6e5c7cef7cca1b5a2", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.14" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.0.16" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2025-05-12T16:37:16+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-07T06:57:01+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-12-05T09:17:50+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:10:34+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-18T13:35:50+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.3.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-12-30T19:00:26+00:00" + }, + { + "name": "symfony/process", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-17T09:11:12+00:00" + }, + { + "name": "symplify/easy-coding-standard", + "version": "12.5.18", + "source": { + "type": "git", + "url": "https://github.com/easy-coding-standard/easy-coding-standard.git", + "reference": "451dfeba3770f2d7476468b891a789c451ae4b34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/451dfeba3770f2d7476468b891a789c451ae4b34", + "reference": "451dfeba3770f2d7476468b891a789c451ae4b34", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "conflict": { + "friendsofphp/php-cs-fixer": "<3.46", + "phpcsstandards/php_codesniffer": "<3.8", + "symplify/coding-standard": "<12.1" + }, + "suggest": { + "ext-dom": "Needed to support checkstyle output format in class CheckstyleOutputFormatter" + }, + "bin": [ + "bin/ecs" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Use Coding Standard with 0-knowledge of PHP-CS-Fixer and PHP_CodeSniffer", + "keywords": [ + "Code style", + "automation", + "fixer", + "static analysis" + ], + "support": { + "issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues", + "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/12.5.18" + }, + "funding": [ + { + "url": "https://www.paypal.me/rectorphp", + "type": "custom" + }, + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2025-05-14T09:38:08+00:00" + }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.5", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "cf6fb197b676ba716837c886baca842e4db29005" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/cf6fb197b676ba716837c886baca842e4db29005", + "reference": "cf6fb197b676ba716837c886baca842e4db29005", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.5" + }, + "time": "2025-04-20T20:23:40+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.4", + "ext-pdo": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/config/container.php b/config/container.php new file mode 100644 index 0000000..84bd30d --- /dev/null +++ b/config/container.php @@ -0,0 +1,24 @@ +addDefinitions([ + Settings::class => $settings, + ]); + // Define container entries + $containerBuilder->addDefinitions( + require __DIR__ . '/definitions.php', + ); + + + + return $containerBuilder->build(); +}; diff --git a/config/definitions.php b/config/definitions.php new file mode 100644 index 0000000..5f9b3e4 --- /dev/null +++ b/config/definitions.php @@ -0,0 +1,93 @@ + fn(Settings $s): Configuration => ORMSetup::createAttributeMetadataConfiguration( + paths: [__DIR__ . '/../src/Entity'], + isDevMode: $s->isTestMode, + proxyDir: __DIR__ . '/../var/cache/doctrine/proxy', + ), + Connection::class => fn( + Configuration $configuration, + Settings $s, + ): Connection => DriverManager::getConnection( + params: $s->isTestMode ? [ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ] : [ + 'driver' => 'pdo_sqlite', + 'path' => __DIR__ . '/../var/database.sqlite', + ], + config: $configuration + ), + EntityManagerInterface::class => fn( + Connection $connection, + Configuration $configuration + ): EntityManagerInterface => new EntityManager($connection, $configuration), + + Twig::class => function (\App\Service\ConfigurationService $config): Twig { + $paths = [__DIR__ . '/../templates']; + $cache = __DIR__ . '/../var/cache/twig'; + + // Ensure cache directory exists + if (!is_dir($cache)) { + mkdir($cache, 0o755, true); + } + + $twig = Twig::create($paths, [ + 'cache' => $cache, + 'auto_reload' => true, + ]); + $twig['appName'] = $config->getConfigByKey(SystemSettingKey::SYSTEM_NAME->value)->getValue(); + return $twig; + }, + + // Logger + Logger::class => function (): Logger { + $logDir = __DIR__ . '/../var/logs'; + + // Ensure log directory exists + if (!is_dir($logDir)) { + mkdir($logDir, 0o755, true); + } + + $logger = new Logger('app'); + + $processor = new UidProcessor(); + $logger->pushProcessor($processor); + + $handler = new StreamHandler( + $logDir . '/app.log', + Logger::DEBUG + ); + $logger->pushHandler($handler); + + return $logger; + }, + + // Repositories + DrinkTypeRepository::class => fn(EntityManagerInterface $entityManager): DrinkTypeRepository => new DrinkTypeRepository($entityManager), + InventoryRecordRepository::class => fn(EntityManagerInterface $entityManager): InventoryRecordRepository => new InventoryRecordRepository($entityManager), + OrderRepository::class => fn(EntityManagerInterface $entityManager): OrderRepository => new OrderRepository($entityManager), + OrderItemRepository::class => fn(EntityManagerInterface $entityManager): OrderItemRepository => new OrderItemRepository($entityManager), + SystemConfigRepository::class => fn(EntityManagerInterface $entityManager): SystemConfigRepository => new SystemConfigRepository($entityManager), +]; diff --git a/docs/database-schema.md b/docs/database-schema.md new file mode 100644 index 0000000..3bf239b --- /dev/null +++ b/docs/database-schema.md @@ -0,0 +1,134 @@ +# Database Schema Design + +This document outlines the database schema for the Drinks Inventory System. + +## Entity Relationship Diagram + +``` ++----------------+ +-------------------+ +----------------+ +| DrinkType | | InventoryRecord | | Order | ++----------------+ +-------------------+ +----------------+ +| id | | id | | id | +| name | | drink_type_id |<----->| created_at | +| description | | quantity | | updated_at | +| desired_stock |<----->| timestamp | | status | +| created_at | | created_at | +----------------+ +| updated_at | | updated_at | ^ ++----------------+ +-------------------+ | + | + | + +----------------+ + | OrderItem | + +----------------+ + | id | + | order_id | + | drink_type_id | + | quantity | + | created_at | + | updated_at | + +----------------+ + + +-------------------+ + | SystemConfig | + +-------------------+ + | id | + | key | + | value | + | created_at | + | updated_at | + +-------------------+ +``` + +## Tables + +### DrinkType + +Stores information about different types of drinks. + +| Column | Type | Constraints | Description | +|----------------|--------------|-------------------|---------------------------------------| +| id | INT | PK, AUTO_INCREMENT| Unique identifier | +| name | VARCHAR(255) | UNIQUE, NOT NULL | Name of the drink type | +| description | TEXT | NULL | Optional description | +| desired_stock | INT | NOT NULL | Desired stock level in crates | +| created_at | DATETIME | NOT NULL | Record creation timestamp | +| updated_at | DATETIME | NOT NULL | Record last update timestamp | + +### InventoryRecord + +Stores the history of inventory changes. + +| Column | Type | Constraints | Description | +|----------------|--------------|-------------------|---------------------------------------| +| id | INT | PK, AUTO_INCREMENT| Unique identifier | +| drink_type_id | INT | FK, NOT NULL | Reference to DrinkType | +| quantity | INT | NOT NULL | Current quantity in crates | +| timestamp | DATETIME | NOT NULL | When the inventory was recorded | +| created_at | DATETIME | NOT NULL | Record creation timestamp | +| updated_at | DATETIME | NOT NULL | Record last update timestamp | + +### Order + +Stores information about orders. + +| Column | Type | Constraints | Description | +|----------------|--------------|-------------------|---------------------------------------| +| id | INT | PK, AUTO_INCREMENT| Unique identifier | +| status | ENUM | NOT NULL | Order status (new, in work, ordered, fulfilled) | +| created_at | DATETIME | NOT NULL | Record creation timestamp | +| updated_at | DATETIME | NOT NULL | Record last update timestamp | + +### OrderItem + +Stores items within an order. + +| Column | Type | Constraints | Description | +|----------------|--------------|-------------------|---------------------------------------| +| id | INT | PK, AUTO_INCREMENT| Unique identifier | +| order_id | INT | FK, NOT NULL | Reference to Order | +| drink_type_id | INT | FK, NOT NULL | Reference to DrinkType | +| quantity | INT | NOT NULL | Quantity to order in crates | +| created_at | DATETIME | NOT NULL | Record creation timestamp | +| updated_at | DATETIME | NOT NULL | Record last update timestamp | + +### SystemConfig + +Stores system configuration parameters. + +| Column | Type | Constraints | Description | +|----------------|--------------|-------------------|---------------------------------------| +| id | INT | PK, AUTO_INCREMENT| Unique identifier | +| key | VARCHAR(255) | UNIQUE, NOT NULL | Configuration key | +| value | TEXT | NOT NULL | Configuration value | +| created_at | DATETIME | NOT NULL | Record creation timestamp | +| updated_at | DATETIME | NOT NULL | Record last update timestamp | + +## Relationships + +1. **DrinkType to InventoryRecord**: One-to-Many + - A drink type can have multiple inventory records + - Each inventory record belongs to one drink type + +2. **DrinkType to OrderItem**: One-to-Many + - A drink type can be included in multiple order items + - Each order item refers to one drink type + +3. **Order to OrderItem**: One-to-Many + - An order can contain multiple order items + - Each order item belongs to one order + +## Indexes + +- `drink_type_id` in `InventoryRecord` table +- `order_id` in `OrderItem` table +- `drink_type_id` in `OrderItem` table +- `key` in `SystemConfig` table + +## Configuration Parameters + +The `SystemConfig` table will store the following configuration parameters: + +- `stock_adjustment_lookback`: How many past orders and the stock at their time should be considered +- `stock_adjustment_magnitude`: How much to adjust desired stock levels by +- `stock_adjustment_threshold`: Threshold for triggering adjustments +- Other system-wide configuration parameters \ No newline at end of file diff --git a/docs/development-guidelines.md b/docs/development-guidelines.md new file mode 100644 index 0000000..12364b1 --- /dev/null +++ b/docs/development-guidelines.md @@ -0,0 +1,103 @@ +# Development Guidelines + +This document outlines the development workflow and guidelines for the Drinks Inventory System. + +## Development Environment Setup + +### Requirements +- PHP 8.4 or higher +- Composer +- SQLite +- Git + +### General + +- Use classes for entities, repositories, services and controllers +- Use Doctrine ORM for database interactions +- Use twig for templates +- Register services in the container and use dependency injection +- Write unit as well as feature tests +- Always use DateTimeImmutable unless there is a good reason not to +- Leverage modern php features such as enums, property promotion, and property hooks + +### PHP Constructor Property Promotion and Readonly Properties + +- Use constructor property promotion for all properties that are initialized in the constructor +- Use readonly properties for properties that should not be modified after initialization +- Only use readonly for properties that don't have setter methods +- Example: + ```php + class Example + { + private ?int $id = null; + + public function __construct( + private string $name, + private readonly DateTimeImmutable $createdAt = new DateTimeImmutable(), + private DateTimeImmutable $updatedAt = new DateTimeImmutable() + ) { + } + + // Getter for readonly property + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + // Getter and setter for non-readonly property + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + $this->updateTimestamp(); + return $this; + } + + private function updateTimestamp(): void + { + $this->updatedAt = new DateTimeImmutable(); + } +} + +### Doctrine ORM Guidelines + +- Use PHP 8 attributes for entity mapping (not annotations or XML) +- Place all entity classes in the `src/Entity` directory +- Place all repository classes in the `src/Repository` directory +- Use the naming convention `EntityNameRepository` for repository classes +- Define relationships between entities using appropriate Doctrine annotations (`OneToMany`, `ManyToOne`, etc.) +- Use `readonly` for properties that should not change after entity creation (like `createdAt`) +- Always include `created_at` and `updated_at` fields in all entities +- Implement an `updateTimestamp()` method to update the `updated_at` field when an entity is modified +- Use the repository pattern for database operations, not direct entity manager operations in services +- Define custom finder methods in repository classes for specific query needs +- Use Doctrine's query builder for complex queries +- Always validate entity data before persisting to the database + +Example entity with Doctrine attributes: +```php +#[ORM\Entity(repositoryClass: ExampleRepository::class)] +#[ORM\Table(name: 'example')] +class Example +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private ?int $id = null; + + #[ORM\Column(type: 'string', length: 255)] + private string $name; + + #[ORM\Column(type: 'datetime_immutable')] + private readonly DateTimeImmutable $createdAt; + + #[ORM\Column(type: 'datetime_immutable')] + private DateTimeImmutable $updatedAt; + + // Constructor, getters, setters, etc. +} +``` diff --git a/docs/directory-structure.md b/docs/directory-structure.md new file mode 100644 index 0000000..111f3e8 --- /dev/null +++ b/docs/directory-structure.md @@ -0,0 +1,127 @@ +# Directory Structure Proposal + +This document outlines the proposed directory structure for the Drinks Inventory System. + +``` +saufen/ +├── docs/ # Documentation +│ ├── database-schema.md +│ ├── development-guidelines.md +│ ├── feature-specifications.md +│ ├── implementation-tasks.md +│ └── directory-structure.md +├── public/ # Public web root +│ ├── index.php # Entry point +│ └── assets/ # Static assets +│ ├── css/ # CSS files +│ ├── js/ # JavaScript files +│ └── images/ # Image files +├── config/ # Configuration files +│ ├── container.php # DI container factory +│ ├── definitions.php # Container Definitions +│ └── database.php # Database configuration +├── src/ # Application source code +│ ├── bootstrap.php # Application bootstrap +│ ├── routes.php # Route definitions +│ ├── Entity/ # Entity classes +│ │ ├── DrinkType.php +│ │ ├── InventoryRecord.php +│ │ ├── Order.php +│ │ ├── OrderItem.php +│ │ └── SystemConfig.php +│ ├── Repository/ # Repository classes +│ │ ├── DrinkTypeRepository.php +│ │ ├── InventoryRecordRepository.php +│ │ ├── OrderRepository.php +│ │ ├── OrderItemRepository.php +│ │ └── SystemConfigRepository.php +│ ├── Service/ # Service classes +│ │ ├── DrinkTypeService.php +│ │ ├── InventoryService.php +│ │ ├── OrderService.php +│ │ ├── StockAdjustmentService.php +│ │ └── ConfigurationService.php +│ ├── Controller/ # Controller classes +│ │ ├── DashboardController.php +│ │ ├── DrinkTypeController.php +│ │ ├── InventoryController.php +│ │ ├── OrderController.php +│ │ └── SettingsController.php +│ ├── Middleware/ # Middleware classes +│ │ └── ErrorHandlerMiddleware.php +├── templates/ # Twig templates +│ ├── layout.twig # Base layout +│ ├── components/ # Reusable components +│ │ ├── navigation.twig +│ │ ├── flash-messages.twig +│ │ └── form-elements.twig +│ ├── dashboard/ +│ │ └── index.twig +│ ├── drink-types/ +│ │ ├── index.twig +│ │ ├── form.twig +│ │ └── detail.twig +│ ├── inventory/ +│ │ ├── index.twig +│ │ ├── update-form.twig +│ │ └── history.twig +│ ├── orders/ +│ │ ├── index.twig +│ │ ├── form.twig +│ │ ├── detail.twig +│ │ └── status-form.twig +│ └── settings/ +│ └── index.twig +├── tests/ # Test files +│ └── Feature/ # Feature tests +│ ├── Controller/ +│ └── Integration/ +│ └── Service/ +├── var/ # Variable data +│ ├── cache/ # Cache files +│ ├── logs/ # Log files +│ └── database.sqlite # SQLite database file +├── vendor/ # Composer dependencies +├── composer.json # Composer configuration +├── composer.lock # Composer lock file +├── ecs.php # Easy Coding Standard config +└── rector.php # Rector config +``` + +## Key Directories and Files + +### src/ +Contains all the application source code, organized by component type. + +- **Entity/**: Contains the entity classes that represent the database tables. +- **Repository/**: Contains the repository classes that handle database operations. +- **Service/**: Contains the service classes that implement business logic. +- **Controller/**: Contains the controller classes that handle HTTP requests. +- **Middleware/**: Contains middleware classes for request/response processing. +- **Util/**: Contains utility classes and helper functions. +- **config/**: Contains configuration files. + +### templates/ +Contains all the Twig templates, organized by feature. + +- **layout.twig**: The base layout template. +- **components/**: Reusable template components. +- **dashboard/**, **drink-types/**, **inventory/**, **orders/**, **settings/**: Feature-specific templates. + +### public/ +Contains the web root and static assets. + +- **index.php**: The application entry point. +- **assets/**: Static assets like CSS, JavaScript, and images. + +### tests/ +Contains all test files, organized by test type. + +- **Unit/**: Unit tests for individual classes. +- **Feature/**: Feature tests for controllers and integration tests. + +### var/ +Contains variable data like cache files, logs, and the SQLite database file. + +### docs/ +Contains project documentation. \ No newline at end of file diff --git a/docs/feature-specifications.md b/docs/feature-specifications.md new file mode 100644 index 0000000..339d620 --- /dev/null +++ b/docs/feature-specifications.md @@ -0,0 +1,135 @@ +# Feature Specifications + +This document outlines the features and requirements for the Drinks Inventory System. + +## Core Features + +### 1. Drink Type Management + +#### Description +Allow users to manage different types of drinks in the inventory system. + +#### User Stories +- As a user, I want to add a new type of drink to the system +- As a user, I want to edit existing drink types +- As a user, I want to view a list of all drink types +- As a user, I want to specify the desired amount of crates for each drink type + +#### Acceptance Criteria +- Users can create new drink types with the following information: + - Name + - Description (optional) + - Desired stock level (in crates) +- Users can edit existing drink types +- Users can view a list of all drink types with their current and desired stock levels +- The system validates that drink names are unique + +### 2. Inventory Management + +#### Description +Allow users to manage the current inventory of drinks. + +#### User Stories +- As a user, I want to update the current stock level of a drink +- As a user, I want to view the current stock levels of all drinks +- As a user, I want to see the history of stock changes for each drink + +#### Acceptance Criteria +- Users can update the current stock level of a drink +- The system records each stock update with a timestamp +- Users can view a history of stock changes for each drink +- The system displays the difference between current and desired stock levels + +### 3. Order Management + +#### Description +Allow users to create and manage orders for drinks. + +#### User Stories +- As a user, I want the system to automatically calculate how much of each drink to order +- As a user, I want to create a new order +- As a user, I want to update the status of an order +- As a user, I want to view all orders and their statuses + +#### Acceptance Criteria +- The system automatically calculates order quantities based on: + - Current stock levels + - Desired stock levels +- Users can create new orders with the following states: + - New + - In Work + - Ordered + - Fulfilled +- Users can update the status of an order +- Users can view all orders with their details and current status + +### 4. Adaptive Stock Level Adjustment + +#### Description +The system should automatically adjust desired stock levels based on consumption patterns. + +#### User Stories +- As a user, I want the system to automatically adjust desired stock levels based on consumption patterns +- As a user, I want to configure the parameters for automatic stock level adjustments + +#### Acceptance Criteria +- The system analyzes consumption patterns when a new order is created +- If a drink is consistently overstocked at order time, the system decreases its desired stock level +- If a drink is consistently understocked at order time, the system increases its desired stock level +- Users can configure the parameters for these adjustments, including: + - Adjustment frequency + - Adjustment magnitude + - Threshold for triggering adjustments + +## User Interface Requirements + +### General UI Requirements +- Simple, intuitive interface +- Mobile-friendly design +- Clear visual indicators for stock levels (e.g., color coding for low stock) +- Responsive design for use on different devices + +### Main Views +1. **Dashboard** + - Overview of current stock levels + - Alerts for low stock items + - Quick update form to update current stock levels + - Quick access to create new orders + +2. **Drink Types Management** + - List of all drink types + - Form for adding/editing drink types + - Display of current and desired stock levels + +3. **Inventory Management** + - Interface for updating current stock levels + - History of stock changes + - Stock level trends over time + +4. **Order Management** + - List of all orders with their statuses + - Interface for creating new orders + - Interface for updating order statuses + +5. **Settings** + - Configuration for automatic stock level adjustments + - System preferences + +## Non-Functional Requirements + +### Performance +- Page load times should be under 2 seconds +- Database queries should be optimized for performance + +### Usability +- The system should be usable without training +- Forms should include validation and helpful error messages +- The interface should be accessible + +### Security +- The system will be used internally without login +- Input validation to prevent SQL injection and other attacks + +### Reliability +- The system should handle concurrent users +- Data should be backed up regularly diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md new file mode 100644 index 0000000..e28be6c --- /dev/null +++ b/docs/implementation-plan.md @@ -0,0 +1,128 @@ +# Implementation Plan with Timeline + +This document outlines a proposed implementation plan with timeline for the Drinks Inventory System. + +## Phase 1: Project Setup and Foundation (Week 1) + +### Week 1: Setup and Basic Structure +- **Days 1-2: Project Setup** + - Set up database connection and configuration + - Create database schema migration scripts + - Configure dependency injection container + - Set up Twig template engine + - Create base layout template + - Set up error handling and logging + +- **Days 3-5: Entity and Repository Classes** + - Create all entity classes (DrinkType, InventoryRecord, Order, OrderItem, SystemConfig) + - Create all repository classes with basic CRUD operations + - Set up database schema and initial migrations + - Write unit tests for entities and repositories + +## Phase 2: Core Features Implementation (Weeks 2-3) + +### Week 2: Drink Type and Inventory Management +- **Days 1-2: Service Layer** + - Implement DrinkTypeService + - Implement InventoryService + - Write unit tests for services + +- **Days 3-5: Controllers and Templates** + - Implement DrinkTypeController with routes + - Implement InventoryController with routes + - Create templates for drink type management + - Create templates for inventory management + - Write feature tests for controllers + +### Week 3: Order Management +- **Days 1-3: Order Management Implementation** + - Implement OrderService with automatic order calculation + - Implement OrderController with routes + - Create templates for order management + - Write unit and feature tests + +- **Days 4-5: Dashboard Implementation** + - Implement DashboardController + - Create dashboard template with overview of stock levels + - Implement low stock alerts + - Create quick update form for inventory + +## Phase 3: Advanced Features and Refinement (Weeks 4-5) + +### Week 4: Adaptive Stock Level and Settings +- **Days 1-3: Adaptive Stock Level Adjustment** + - Implement StockAdjustmentService + - Implement consumption pattern analysis + - Implement automatic stock level adjustment + - Write unit tests for stock adjustment + +- **Days 4-5: Settings Implementation** + - Implement ConfigurationService + - Implement SettingsController + - Create settings templates + - Write tests for configuration + +### Week 5: UI Enhancements and Refinement +- **Days 1-3: UI Enhancements** + - Implement responsive design + - Add color coding for stock levels + - Implement mobile-friendly interface + - Add form validation and error messages + - Implement loading indicators + +- **Days 4-5: Testing and Bug Fixes** + - Conduct comprehensive testing + - Fix identified bugs + - Optimize performance + - Refine user experience + +## Phase 4: Documentation and Deployment (Week 6) + +### Week 6: Documentation and Deployment +- **Days 1-3: Documentation** + - Create user documentation + - Document API endpoints + - Document configuration options + - Update technical documentation + +- **Days 4-5: Deployment** + - Set up production environment + - Configure database for production + - Set up backup system + - Configure error logging for production + - Perform final testing in production environment + +## Development Approach + +### Iterative Development +- Each feature will be developed following these steps: + 1. Implement entity and repository classes + 2. Implement service layer with business logic + 3. Implement controller with routes + 4. Create templates and UI components + 5. Write tests + 6. Review and refine + +### Testing Strategy +- Unit tests for all entity, repository, and service classes +- Feature tests for controllers and UI flows +- Integration tests for database operations +- Manual testing for UI and user experience + +### Code Quality Assurance +- Follow PSR coding standards +- Use static analysis tools (PHPStan, Rector) +- Conduct code reviews +- Maintain high test coverage + +## Risk Management + +### Potential Risks and Mitigation Strategies +- **Scope Creep**: Strictly adhere to the defined requirements and manage change requests +- **Technical Challenges**: Allocate additional time for complex features +- **Integration Issues**: Implement comprehensive integration testing +- **Performance Issues**: Conduct performance testing early and optimize as needed + +## Conclusion + +This implementation plan provides a structured approach to developing the Drinks Inventory System over a 6-week period. The phased approach allows for incremental development and testing, ensuring that each component is properly implemented and integrated before moving on to the next phase. \ No newline at end of file diff --git a/docs/implementation-tasks.md b/docs/implementation-tasks.md new file mode 100644 index 0000000..97bc8cb --- /dev/null +++ b/docs/implementation-tasks.md @@ -0,0 +1,166 @@ +# Implementation Tasks for Drinks Inventory System + +This document outlines the tasks required to implement the Drinks Inventory System based on the feature specifications, database schema, and development guidelines. +Always run our qualitytools after implementing a step: +run: +- composer lint +- composer test + +## 1. Project Setup and Configuration + +- [x] Set up database connection and configuration +- [x] Configure dependency injection container +- [x] Set up Twig template engine +- [x] Create base layout template +- [x] Set up error handling and logging + +## 2. Entity Classes + +- [x] Create DrinkType entity class +- [x] Create InventoryRecord entity class +- [x] Create Order entity class +- [x] Create OrderItem entity class +- [x] Create SystemConfig entity class + +## 3. Repository Classes + +- [x] Create DrinkTypeRepository +- [x] Create InventoryRecordRepository +- [x] Create OrderRepository +- [x] Create OrderItemRepository +- [x] Create SystemConfigRepository + +## 4. Service Classes + +- [x] Create DrinkTypeService +- [x] Create InventoryService +- [x] Create OrderService +- [x] Create StockAdjustmentService +- [x] Create ConfigurationService + +## 5. Controller Classes + +- [x] Create DashboardController +- [x] Create DrinkTypeController +- [x] Create InventoryController +- [x] Create OrderController +- [x] Create SettingsController + +## 6. Templates + +### 6.1 Layout and Common Components +- [x] Create base layout template +- [x] Create navigation component +- [x] Create flash message component +- [x] Create form components (inputs, buttons, etc.) + +### 6.2 Dashboard +- [x] Create dashboard template with overview of stock levels +- [x] Create low stock alerts component +- [x] Create quick update form + +### 6.3 Drink Types +- [x] Create drink type list template +- [x] Create drink type form template (add/edit) +- [x] Create drink type detail template + +### 6.4 Inventory +- [x] Create inventory overview template +- [x] Create stock update form +- [x] Create inventory history template + +### 6.5 Orders +- [x] Create order list template +- [x] Create order creation form +- [x] Create order detail template +- [x] Create order status update form + +### 6.6 Settings +- [x] Create settings form template + +## 7. Routes + +- [x] Configure dashboard routes +- [x] Configure drink type routes +- [x] Configure inventory routes +- [x] Configure order routes +- [x] Configure settings routes + +## 8. Feature Implementation + +### 8.1 Drink Type Management +- [ ] Implement adding new drink types +- [ ] Implement editing existing drink types +- [ ] Implement listing all drink types +- [ ] Implement validation for drink type uniqueness + +### 8.2 Inventory Management +- [ ] Implement updating current stock levels +- [ ] Implement viewing current stock levels +- [ ] Implement viewing stock change history +- [ ] Implement displaying difference between current and desired stock + +### 8.3 Order Management +- [ ] Implement automatic order quantity calculation +- [ ] Implement order creation +- [ ] Implement order status updates +- [ ] Implement order listing and details + +### 8.4 Adaptive Stock Level Adjustment +- [x] Implement consumption pattern analysis + - [x] Analyze the last 5 orders for each drink type + - [x] Apply weighted importance (most recent orders count more) + - [x] Calculate suggested stock levels based on consumption patterns +- [x] Implement automatic stock level adjustment + - [x] Adjust desired stock levels based on weighted consumption + - [x] Apply gradual adjustments to avoid drastic changes + - [x] Ensure minimum stock levels are maintained +- [x] Implement configuration for adjustment parameters + - [x] Configure number of orders to analyze (default: 5) + - [x] Configure adjustment magnitude (default: 20%) + - [x] Configure adjustment threshold (default: 10%) + +## 9. UI Enhancements + +- [x] Implement responsive design +- [ ] Add color coding for stock levels +- [x] Implement mobile-friendly interface +- [ ] Add form validation and error messages +- [ ] Implement loading indicators + +## 10. Testing + +- [ ] Write unit tests for entity classes +- [ ] Write unit tests for repository classes +- [ ] Write unit tests for service classes +- [ ] Write feature tests for controllers +- [ ] Write integration tests for database operations + +## 11. Documentation + +- [ ] Create user documentation +- [ ] Document API endpoints +- [ ] Document database schema +- [ ] Document configuration options + +## 12. Deployment + +- [ ] Set up production environment +- [ ] Configure database for production +- [ ] Set up backup system +- [ ] Configure error logging for production + +## Implementation Order + +For efficient development, the following implementation order is recommended: + +1. Project setup and configuration +2. Entity and repository classes +3. Basic service classes +4. Basic templates and controllers +5. Core features (Drink Type and Inventory Management) +6. Order Management +7. Adaptive Stock Level Adjustment +8. UI enhancements +9. Testing and bug fixes +10. Documentation and deployment diff --git a/docs/project-summary.md b/docs/project-summary.md new file mode 100644 index 0000000..a94f9ca --- /dev/null +++ b/docs/project-summary.md @@ -0,0 +1,77 @@ +# Drinks Inventory System - Project Summary + +## Project Overview + +The Drinks Inventory System is a web application designed to manage the inventory of drinks, track stock levels, create orders, and automatically adjust desired stock levels based on consumption patterns. The system will be built using PHP 8.4+, Slim framework, Twig templates, and SQLite database. + +## Key Documents + +This project is documented through several key files: + +1. **[Feature Specifications](feature-specifications.md)**: Outlines the core features and requirements of the system. +2. **[Database Schema](database-schema.md)**: Defines the database structure and relationships. +3. **[Development Guidelines](development-guidelines.md)**: Provides guidelines for development workflow and standards. +4. **[Implementation Tasks](implementation-tasks.md)**: Lists all tasks required to implement the system. +5. **[Directory Structure](directory-structure.md)**: Proposes an organized directory structure for the project. +6. **[Implementation Plan](implementation-plan.md)**: Outlines a timeline and approach for implementing the system. + +## Core Features + +The system includes four core features: + +1. **Drink Type Management**: Adding, editing, and viewing drink types with desired stock levels. +2. **Inventory Management**: Updating and viewing current stock levels and history. +3. **Order Management**: Creating orders based on stock levels and tracking their status. +4. **Adaptive Stock Level Adjustment**: Automatically adjusting desired stock levels based on consumption patterns. + +## Technical Architecture + +The application follows a layered architecture: + +1. **Entity Layer**: Represents the database tables (DrinkType, InventoryRecord, Order, OrderItem, SystemConfig). +2. **Repository Layer**: Handles database operations for each entity. +3. **Service Layer**: Implements business logic and coordinates between repositories. +4. **Controller Layer**: Handles HTTP requests and responses. +5. **View Layer**: Twig templates for rendering the UI. + +## Database Design + +The database consists of five tables: + +1. **DrinkType**: Stores information about different types of drinks. +2. **InventoryRecord**: Stores the history of inventory changes. +3. **Order**: Stores information about orders. +4. **OrderItem**: Stores items within an order. +5. **SystemConfig**: Stores system configuration parameters. + +## Implementation Approach + +The implementation will follow an iterative approach over a 6-week period: + +1. **Phase 1 (Week 1)**: Project setup and foundation +2. **Phase 2 (Weeks 2-3)**: Core features implementation +3. **Phase 3 (Weeks 4-5)**: Advanced features and refinement +4. **Phase 4 (Week 6)**: Documentation and deployment + +## Development Practices + +The project will adhere to the following development practices: + +1. **Object-Oriented Design**: Using classes for entities, repositories, services, and controllers. +2. **Dependency Injection**: Registering services in the container and using dependency injection. +3. **Testing**: Writing unit and feature tests for all components. +4. **Code Quality**: Following PSR coding standards and using static analysis tools. +5. **Version Control**: Using Git for version control. + +## Next Steps + +To begin implementation, follow these steps: + +1. Set up the project structure according to the [Directory Structure](directory-structure.md) document. +2. Implement the database connection and schema migrations. +3. Create the entity and repository classes. +4. Follow the [Implementation Plan](implementation-plan.md) for subsequent steps. + +## Conclusion + +This project summary provides an overview of the Drinks Inventory System, its features, architecture, and implementation approach. The detailed documents referenced in this summary provide comprehensive information for implementing the system. \ No newline at end of file diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..5cdbca7 --- /dev/null +++ b/ecs.php @@ -0,0 +1,33 @@ +withPaths([ + __DIR__ . '/public', + __DIR__ . '/src', + __DIR__ . '/config', + __DIR__ . '/tests', + __DIR__ . '/bin', + ]) + + // add a single rule + ->withRules([ + NoUnusedImportsFixer::class, + ]) + + ->withPhpCsFixerSets( + per: true, + php84Migration: true, + ) + + + // add sets - group of rules, from easiest to more complex ones + // uncomment one, apply one, commit, PR, merge and repeat + ->withPreparedSets( + strict: true, + ) + ; diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..1aa670f --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: 9 + paths: + - src + - config + - tests \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e6198e0 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests + + + + + app + src + + + diff --git a/public/assets/css/styles.css b/public/assets/css/styles.css new file mode 100644 index 0000000..185a46e --- /dev/null +++ b/public/assets/css/styles.css @@ -0,0 +1,488 @@ +/* Pride-themed Bootstrap color overrides */ +:root { + /* Pride flag colors with RGB values */ + --pride-red: #e40303; + --pride-red-rgb: 228, 3, 3; + + --pride-orange: #ff8c00; + --pride-orange-rgb: 255, 140, 0; + + --pride-yellow: #ffed00; + --pride-yellow-rgb: 255, 237, 0; + + --pride-green: #008026; + --pride-green-rgb: 0, 128, 38; + + --pride-blue: #004dff; + --pride-blue-rgb: 0, 77, 255; + + --pride-purple: #750787; + --pride-purple-rgb: 117, 7, 135; + + /* Additional pride colors */ + --pride-pink: #ff69b4; + --pride-pink-rgb: 255, 105, 180; + + --pride-cyan: #00ffff; + --pride-cyan-rgb: 0, 255, 255; + + --pride-lavender: #b57edc; + --pride-lavender-rgb: 181, 126, 220; + + /* Override Bootstrap's default colors with pride colors */ + --bs-primary: var(--pride-blue); + --bs-primary-rgb: 0, 77, 255; + + --bs-secondary: var(--pride-purple); + --bs-secondary-rgb: 117, 7, 135; + + --bs-success: var(--pride-green); + --bs-success-rgb: 0, 128, 38; + + --bs-info: #3ECDF3; /* Light blue from trans flag */ + --bs-info-rgb: 62, 205, 243; + + --bs-warning: var(--pride-orange); + --bs-warning-rgb: 255, 140, 0; + + --bs-danger: var(--pride-red); + --bs-danger-rgb: 228, 3, 3; + + /* Replace light and dark with more colorful alternatives */ + --bs-light: #f0e6ff; /* Light lavender */ + --bs-light-rgb: 240, 230, 255; + + --bs-dark: #330066; /* Deep purple */ + --bs-dark-rgb: 51, 0, 102; + + /* Link colors */ + --bs-link-color: var(--pride-blue); + --bs-link-hover-color: var(--pride-purple); + + /* Button hover effects */ + --bs-btn-hover-bg-shade-amount: 15%; + --bs-btn-hover-bg-tint-amount: 15%; + + /* Card and border colors */ + --bs-border-color: rgba(var(--bs-primary-rgb), 0.2); + --bs-card-cap-bg: rgba(var(--bs-primary-rgb), 0.1); + + /* Navbar colors */ + --bs-navbar-active-color: var(--pride-yellow); + --bs-navbar-brand-color: var(--pride-yellow); + --bs-navbar-brand-hover-color: var(--pride-orange); + --bs-navbar-hover-color: var(--pride-orange); +} + +/* Custom background classes for pride colors */ +.bg-pride-red { background-color: var(--pride-red) !important; color: white !important; } +.bg-pride-orange { background-color: var(--pride-orange) !important; color: black !important; } +.bg-pride-yellow { background-color: var(--pride-yellow) !important; color: black !important; } +.bg-pride-green { background-color: var(--pride-green) !important; color: white !important; } +.bg-pride-blue { background-color: var(--pride-blue) !important; color: white !important; } +.bg-pride-purple { background-color: var(--pride-purple) !important; color: white !important; } +.bg-pride-pink { background-color: var(--pride-pink) !important; color: white !important; } +.bg-pride-cyan { background-color: var(--pride-cyan) !important; color: black !important; } +.bg-pride-lavender { background-color: var(--pride-lavender) !important; color: black !important; } + +/* Body styling with subtle gradient background */ +body { + background: linear-gradient(135deg, var(--bs-light) 0%, white 50%, var(--bs-light) 100%); + min-height: 100vh; +} + +/* Rainbow gradient backgrounds */ +.bg-rainbow { + background: linear-gradient( + to right, + var(--pride-red), + var(--pride-orange), + var(--pride-yellow), + var(--pride-green), + var(--pride-blue), + var(--pride-purple) + ) !important; + color: white !important; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); +} + +.bg-rainbow-vertical { + background: linear-gradient( + to bottom, + var(--pride-red), + var(--pride-orange), + var(--pride-yellow), + var(--pride-green), + var(--pride-blue), + var(--pride-purple) + ) !important; + color: white !important; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); +} + +/* Override specific Bootstrap components */ +.navbar-dark { + background: linear-gradient( + to right, + var(--pride-red), + var(--pride-orange), + var(--pride-yellow), + var(--pride-green), + var(--pride-blue), + var(--pride-purple) + ) !important; +} + +.navbar-dark .navbar-brand, +.navbar-dark .nav-link { + color: white !important; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); +} + +.navbar-dark .nav-link:hover { + color: var(--bs-light) !important; + text-shadow: 0 0 5px white; +} + +/* Card styling */ +.card { + border-radius: 10px; + overflow: hidden; + box-shadow: 0 4px 15px rgba(var(--pride-lavender-rgb), 0.3); + transition: transform 0.3s ease, box-shadow 0.3s ease; + border: none; + background-color: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(5px); +} + +.card:hover { + transform: translateY(-20px); + box-shadow: 0 15px 30px rgba(var(--pride-pink-rgb), 0.4); +} + +.card-header { + background: linear-gradient( + to right, + rgba(var(--pride-blue-rgb), 0.7), + rgba(var(--pride-purple-rgb), 0.7) + ); + color: white; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); + border-bottom: none; + padding: 1rem 1.25rem; +} + +.card-body { + border-top: 3px solid rgba(255, 255, 255, 0.8); +} + +/* Add rainbow border to cards */ +.card { + border: 3px solid transparent; + background-clip: padding-box; + position: relative; +} + +.card::after { + content: ''; + position: absolute; + top: -3px; + left: -3px; + right: -3px; + bottom: -3px; + z-index: -1; + border-radius: 12px; + background: linear-gradient( + 45deg, + var(--pride-red), + var(--pride-orange), + var(--pride-yellow), + var(--pride-green), + var(--pride-blue), + var(--pride-purple), + var(--pride-pink) + ); +} + +/* Button styling */ +.btn { + border-radius: 25px; + font-weight: bold; + transition: all 0.3s ease; +} + +.btn:hover { + transform: scale(1.2); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +/* Footer styling */ +footer { + background: linear-gradient( + to right, + var(--pride-purple), + var(--pride-blue), + var(--pride-green), + var(--pride-yellow), + var(--pride-orange), + var(--pride-red) + ) !important; + color: white !important; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); + padding: 1.5rem 0; + margin-top: 2rem; +} + +/* Table styling */ +.table { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(var(--bs-primary-rgb), 0.1); +} + +.table thead { + background: linear-gradient( + to right, + rgba(var(--pride-pink-rgb), 0.7), + rgba(var(--bs-primary-rgb), 0.7) + ); + color: white; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} + +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: rgba(var(--pride-lavender-rgb), 0.15); +} + +.table-striped > tbody > tr:nth-of-type(even) { + background-color: rgba(var(--pride-cyan-rgb), 0.1); +} + +.table-hover > tbody > tr:hover { + background-color: rgba(var(--bs-warning-rgb), 0.2); + transform: scale(1.01); + transition: all 0.2s ease; +} + +/* Badge styling */ +.badge { + border-radius: 12px; + padding: 0.5em 0.8em; + font-weight: 600; +} + +/* Progress bar styling */ +.progress { + height: 1.2rem; + border-radius: 0.6rem; + background-color: rgba(var(--bs-primary-rgb), 0.1); +} + +.progress-bar { + border-radius: 0.6rem; +} + +/* Alert styling */ +.alert { + border-radius: 10px; + border: none; + padding: 1rem 1.5rem; + margin-bottom: 1.5rem; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; + color: #000; + font-weight: 500; + text-shadow: 0 0 1px rgba(255, 255, 255, 0.5); +} + +.alert::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 5px; + height: 100%; +} + +.alert-primary { + background-color: rgba(var(--pride-blue-rgb), 0.2); +} + +.alert-primary::before { + background-color: var(--pride-blue); +} + +.alert-secondary { + background-color: rgba(var(--pride-purple-rgb), 0.2); +} + +.alert-secondary::before { + background-color: var(--pride-purple); +} + +.alert-success { + background-color: rgba(var(--pride-green-rgb), 0.2); +} + +.alert-success::before { + background-color: var(--pride-green); +} + +.alert-danger { + background-color: rgba(var(--pride-red-rgb), 0.2); +} + +.alert-danger::before { + background-color: var(--pride-red); +} + +.alert-warning { + background-color: rgba(var(--pride-orange-rgb), 0.2); +} + +.alert-warning::before { + background-color: var(--pride-orange); +} + +.alert-info { + background-color: rgba(var(--bs-info-rgb), 0.2); +} + +.alert-info::before { + background-color: var(--bs-info); +} + +/* Form control styling */ +.form-control { + border-radius: 10px; + border: 2px solid rgba(var(--pride-lavender-rgb), 0.3); + transition: all 0.3s ease; +} + +.form-control:focus { + border-color: var(--pride-blue); + box-shadow: 0 0 0 0.25rem rgba(var(--pride-blue-rgb), 0.25); + transform: translateY(-3px); +} + +.form-label { + font-weight: bold; + color: var(--pride-purple); +} + +.form-select { + border-radius: 10px; + border: 2px solid rgba(var(--pride-lavender-rgb), 0.3); + background-image: linear-gradient(45deg, var(--pride-purple) 0%, var(--pride-blue) 100%); + background-size: 20px 20px; + color: var(--bs-dark); + transition: all 0.3s ease; +} + +.form-select:focus { + border-color: var(--pride-blue); + box-shadow: 0 0 0 0.25rem rgba(var(--pride-blue-rgb), 0.25); + transform: translateY(-3px); +} + +/* Rainbow text for special elements */ +.rainbow-text { + background-image: linear-gradient( + to right, + var(--pride-red), + var(--pride-orange), + var(--pride-yellow), + var(--pride-green), + var(--pride-blue), + var(--pride-purple), + var(--pride-pink) + ); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + font-weight: bold; + animation: rainbow-shift 5s linear infinite; +} + +/* Apply rainbow text to headings */ +h1, h2, h3 { + background-image: linear-gradient( + to right, + var(--pride-red), + var(--pride-orange), + var(--pride-yellow), + var(--pride-green), + var(--pride-blue), + var(--pride-purple), + var(--pride-pink) + ); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + font-weight: bold; +} + +/* Flash messages styling */ +.flash-messages { + margin-bottom: 2rem; +} + +.flash-message { + padding: 1rem; + margin-bottom: 1rem; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + animation: flash-pulse 2s infinite; + font-weight: bold; + text-align: center; +} + +.flash-success { + background: linear-gradient(to right, var(--pride-green), var(--pride-cyan)); + color: white; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} + +.flash-error { + background: linear-gradient(to right, var(--pride-red), var(--pride-pink)); + color: white; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} + +.flash-warning { + background: linear-gradient(to right, var(--pride-yellow), var(--pride-orange)); + color: black; + text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.5); +} + +.flash-info { + background: linear-gradient(to right, var(--pride-blue), var(--pride-lavender)); + color: white; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} + +/* Animations */ +@keyframes rainbow-shift { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +@keyframes flash-pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.02); + } + 100% { + transform: scale(1); + } +} diff --git a/public/assets/js/app.js b/public/assets/js/app.js new file mode 100644 index 0000000..67d4d33 --- /dev/null +++ b/public/assets/js/app.js @@ -0,0 +1,21 @@ +/** + * Main application JavaScript file + */ +document.addEventListener('DOMContentLoaded', function() { + // Auto-hide flash messages after 5 seconds + const flashMessages = document.querySelectorAll('.flash-message'); + if (flashMessages.length > 0) { + setTimeout(function() { + flashMessages.forEach(function(message) { + message.style.opacity = '0'; + message.style.transition = 'opacity 0.5s ease'; + setTimeout(function() { + message.style.display = 'none'; + }, 500); + }); + }, 5000); + } + + // Add any other initialization code here + console.log('Drinks Inventory System initialized'); +}); \ No newline at end of file diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..229fd69 --- /dev/null +++ b/public/index.php @@ -0,0 +1,12 @@ +run(); diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..f270533 --- /dev/null +++ b/rector.php @@ -0,0 +1,27 @@ +withPaths([ + __DIR__ . '/public', + __DIR__ . '/src', + __DIR__ . '/config', + __DIR__ . '/tests', + __DIR__ . '/bin', + ]) + ->withImportNames(removeUnusedImports: true) + ->withPhpSets() + ->withPreparedSets( + codeQuality: true, + typeDeclarations: true, + earlyReturn: true, + strictBooleans: true, + ) + ->withSkip([ + ReadonlyPropertyRector::class, + ]) +; \ No newline at end of file diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php new file mode 100644 index 0000000..d87b1bc --- /dev/null +++ b/src/Controller/DashboardController.php @@ -0,0 +1,41 @@ +inventoryService->getLowStockDrinkTypes(); + + // Get all drink types for the quick-update-form component + $drinkTypes = $this->drinkTypeService->getAllDrinkTypes(); + + // Get configuration settings + $showLowStockAlerts = (bool) $this->configService->getConfigValue('show_low_stock_alerts', '1'); + $showQuickUpdateForm = (bool) $this->configService->getConfigValue('show_quick_update_form', '1'); + + return $this->view->render($response, 'dashboard/index.twig', [ + 'lowStockItems' => $lowStockItems, + 'drinkTypes' => $drinkTypes, + 'showLowStockAlerts' => $showLowStockAlerts, + 'showQuickUpdateForm' => $showQuickUpdateForm + ]); + } +} diff --git a/src/Controller/DrinkTypeController.php b/src/Controller/DrinkTypeController.php new file mode 100644 index 0000000..1c741f6 --- /dev/null +++ b/src/Controller/DrinkTypeController.php @@ -0,0 +1,151 @@ +drinkTypeService->getAllDrinkTypes(); + + // Get current stock levels for all drink types + $drinkTypesWithStock = []; + foreach ($drinkTypes as $drinkType) { + $currentStock = $this->inventoryService->getCurrentStockLevel($drinkType); + $drinkTypesWithStock[] = [ + 'id' => $drinkType->getId(), + 'name' => $drinkType->getName(), + 'description' => $drinkType->getDescription(), + 'desiredStock' => $drinkType->getDesiredStock(), + 'currentStock' => $currentStock, + 'createdAt' => $drinkType->getCreatedAt(), + 'updatedAt' => $drinkType->getUpdatedAt(), + ]; + } + + return $this->view->render($response, 'drink-types/index.twig', [ + 'drinkTypes' => $drinkTypesWithStock, + ]); + } + + public function show(Response $response, int $id): Response + { + $drinkType = $this->drinkTypeService->getDrinkTypeById($id); + + if (!$drinkType instanceof DrinkType) { + // Redirect to drink types list with error message + // In a real application, you might want to use flash messages + return $response->withHeader('Location', '/drink-types')->withStatus(302); + } + + // Get current stock information for the drink type + $currentStock = $this->inventoryService->getCurrentStockLevel($drinkType); + + // Get inventory records for the drink type + $inventoryRecords = $this->inventoryService->getInventoryRecordsByDrinkType($drinkType); + + return $this->view->render($response, 'drink-types/show.twig', [ + 'drinkType' => $drinkType, + 'currentStock' => $currentStock, + 'inventoryRecords' => $inventoryRecords, + ]); + } + + public function create(Response $response): Response + { + return $this->view->render($response, 'drink-types/create.twig'); + } + + public function store(Request $request, Response $response): Response + { + $data = $request->getParsedBody(); + + $name = $data['name'] ?? ''; + $description = $data['description'] ?? null; + $desiredStock = isset($data['desired_stock']) ? (int) $data['desired_stock'] : 0; + + try { + $this->drinkTypeService->createDrinkType($name, $description, $desiredStock); + + // Redirect to drink types list + return $response->withHeader('Location', '/drink-types')->withStatus(302); + } catch (InvalidArgumentException $e) { + // Render the form again with error message + return $this->view->render($response, 'drink-types/create.twig', [ + 'error' => $e->getMessage(), + 'data' => $data, + ]); + } + } + + public function edit(Request $request, Response $response, int $id): Response + { + $drinkType = $this->drinkTypeService->getDrinkTypeById($id); + + if (!$drinkType instanceof DrinkType) { + // Redirect to drink types list + return $response->withHeader('Location', '/drink-types')->withStatus(302); + } + + return $this->view->render($response, 'drink-types/edit.twig', [ + 'drinkType' => $drinkType, + ]); + } + + public function update(Request $request, Response $response, int $id): Response + { + $drinkType = $this->drinkTypeService->getDrinkTypeById($id); + + if (!$drinkType instanceof DrinkType) { + // Redirect to drink types list + return $response->withHeader('Location', '/drink-types')->withStatus(302); + } + + $data = $request->getParsedBody(); + + $name = $data['name'] ?? null; + $description = $data['description'] ?? null; + $desiredStock = isset($data['desired_stock']) ? (int) $data['desired_stock'] : null; + + try { + $this->drinkTypeService->updateDrinkType($drinkType, $name, $description, $desiredStock); + + // Redirect to drink type details + return $response->withHeader('Location', '/drink-types/' . $id)->withStatus(302); + } catch (InvalidArgumentException $e) { + // Render the form again with error message + return $this->view->render($response, 'drink-types/edit.twig', [ + 'drinkType' => $drinkType, + 'error' => $e->getMessage(), + ]); + } + } + + public function delete(Request $request, Response $response, int $id): Response + { + $drinkType = $this->drinkTypeService->getDrinkTypeById($id); + + if ($drinkType instanceof DrinkType) { + $this->drinkTypeService->deleteDrinkType($drinkType); + } + + // Redirect to drink types list + return $response->withHeader('Location', '/drink-types')->withStatus(302); + } +} diff --git a/src/Controller/InventoryController.php b/src/Controller/InventoryController.php new file mode 100644 index 0000000..2a8257f --- /dev/null +++ b/src/Controller/InventoryController.php @@ -0,0 +1,96 @@ +inventoryService->getAllDrinkTypesWithStockLevels(); + + return $this->view->render($response, 'inventory/index.twig', [ + 'stockLevels' => $stockLevels, + ]); + } + + public function showUpdateForm(Response $response): Response + { + $drinkTypes = $this->drinkTypeService->getAllDrinkTypes(); + + return $this->view->render($response, 'inventory/update.twig', [ + 'drinkTypes' => $drinkTypes, + ]); + } + + public function update(Request $request, Response $response): Response + { + $data = $request->getParsedBody(); + + $drinkTypeId = isset($data['drink_type_id']) ? (int) $data['drink_type_id'] : 0; + $quantity = isset($data['quantity']) ? (int) $data['quantity'] : 0; + + $drinkType = $this->drinkTypeService->getDrinkTypeById($drinkTypeId); + + if (!$drinkType instanceof DrinkType) { + // Redirect to inventory page with error message + return $response->withHeader('Location', '/inventory')->withStatus(302); + } + + $this->inventoryService->updateStockLevel($drinkType, $quantity); + + // Redirect to inventory page + return $response->withHeader('Location', '/inventory')->withStatus(302); + } + + public function history(Response $response, ?int $id = null): Response + { + $drinkTypeId = $id; + + if ($drinkTypeId !== null && $drinkTypeId !== 0) { + $drinkType = $this->drinkTypeService->getDrinkTypeById($drinkTypeId); + + if (!$drinkType instanceof DrinkType) { + // Redirect to inventory page + return $response->withHeader('Location', '/inventory')->withStatus(302); + } + + $inventoryRecords = $this->inventoryService->getInventoryRecordsByDrinkType($drinkType); + + return $this->view->render($response, 'inventory/history.twig', [ + 'drinkType' => $drinkType, + 'inventoryRecords' => $inventoryRecords, + ]); + } + + // If no drink type ID is provided, show history for all drink types + $inventoryRecords = $this->inventoryService->getAllInventoryRecords(); + + return $this->view->render($response, 'inventory/history.twig', [ + 'inventoryRecords' => $inventoryRecords, + ]); + } + + public function lowStock(Response $response): Response + { + $lowStockItems = $this->inventoryService->getLowStockDrinkTypes(); + + return $this->view->render($response, 'inventory/low-stock.twig', [ + 'lowStockItems' => $lowStockItems, + ]); + } +} diff --git a/src/Controller/OrderController.php b/src/Controller/OrderController.php new file mode 100644 index 0000000..ef4d098 --- /dev/null +++ b/src/Controller/OrderController.php @@ -0,0 +1,223 @@ +orderService->getAllOrders(); + + return $this->view->render($response, 'orders/index.twig', [ + 'orders' => $orders, + ]); + } + + public function show(Response $response, int $id): Response + { + $order = $this->orderService->getOrderById($id); + + if (!$order instanceof Order) { + // Redirect to orders list + return $response->withHeader('Location', '/orders')->withStatus(302); + } + + return $this->view->render($response, 'orders/show.twig', [ + 'order' => $order, + ]); + } + + public function create(Response $response): Response + { + $drinkTypes = $this->drinkTypeService->getAllDrinkTypes(); + + return $this->view->render($response, 'orders/create.twig', [ + 'drinkTypes' => $drinkTypes, + ]); + } + + public function store(Request $request, Response $response): Response + { + $data = $request->getParsedBody(); + + $items = []; + + // Process form data to create order items + foreach ($data['items'] ?? [] as $item) { + if (!empty($item['drink_type_id']) && !empty($item['quantity'])) { + $items[] = [ + 'drinkTypeId' => (int) $item['drink_type_id'], + 'quantity' => (int) $item['quantity'], + ]; + } + } + + try { + $order = $this->orderService->createOrder($items); + + // Redirect to order details + return $response->withHeader('Location', '/orders/' . $order->getId())->withStatus(302); + } catch (InvalidArgumentException $e) { + // Render the form again with error message + return $this->view->render($response, 'orders/create.twig', [ + 'error' => $e->getMessage(), + 'drinkTypes' => $this->drinkTypeService->getAllDrinkTypes(), + 'data' => $data, + ]); + } + } + + public function createFromStock(Response $response): Response + { + try { + $order = $this->orderService->createOrderFromStockLevels(); + + // Redirect to order details + return $response->withHeader('Location', '/orders/' . $order->getId())->withStatus(302); + } catch (InvalidArgumentException) { + // Redirect to orders list with error message + return $response->withHeader('Location', '/orders')->withStatus(302); + } + } + + public function updateStatus(Request $request, Response $response, int $id): Response + { + $order = $this->orderService->getOrderById($id); + + if (!$order instanceof Order) { + // Redirect to orders list + return $response->withHeader('Location', '/orders')->withStatus(302); + } + + $data = $request->getParsedBody(); + $status = $data['status'] ?? ''; + + try { + $this->orderService->updateOrderStatus($order, $status); + + // Redirect to order details + return $response->withHeader('Location', '/orders/' . $id)->withStatus(302); + } catch (InvalidArgumentException $e) { + // Render the order details with error message + return $this->view->render($response, 'orders/show.twig', [ + 'order' => $order, + 'error' => $e->getMessage(), + ]); + } + } + + public function addItem(Request $request, Response $response, int $id): Response + { + $order = $this->orderService->getOrderById($id); + + if (!$order instanceof Order) { + // Redirect to orders list + return $response->withHeader('Location', '/orders')->withStatus(302); + } + + $data = $request->getParsedBody(); + $drinkTypeId = isset($data['drink_type_id']) ? (int) $data['drink_type_id'] : 0; + $quantity = isset($data['quantity']) ? (int) $data['quantity'] : 0; + + $drinkType = $this->drinkTypeService->getDrinkTypeById($drinkTypeId); + + if (!$drinkType instanceof DrinkType) { + // Render the order details with error message + return $this->view->render($response, 'orders/show.twig', [ + 'order' => $order, + 'error' => 'Invalid drink type', + ]); + } + + try { + $this->orderService->addOrderItem($order, $drinkType, $quantity); + + // Redirect to order details + return $response->withHeader('Location', '/orders/' . $id)->withStatus(302); + } catch (InvalidArgumentException $e) { + // Render the order details with error message + return $this->view->render($response, 'orders/show.twig', [ + 'order' => $order, + 'error' => $e->getMessage(), + ]); + } + } + + public function removeItem(Response $response, int $id, int $itemId): Response + { + $orderId = $id; + + $order = $this->orderService->getOrderById($orderId); + + if (!$order instanceof Order) { + // Redirect to orders list + return $response->withHeader('Location', '/orders')->withStatus(302); + } + + $orderItem = null; + foreach ($order->getOrderItems() as $item) { + if ($item->getId() === $itemId) { + $orderItem = $item; + break; + } + } + + if (!$orderItem) { + // Redirect to order details + return $response->withHeader('Location', '/orders/' . $orderId)->withStatus(302); + } + + try { + $this->orderService->removeOrderItem($order, $orderItem); + + // Redirect to order details + return $response->withHeader('Location', '/orders/' . $orderId)->withStatus(302); + } catch (InvalidArgumentException $e) { + // Render the order details with error message + return $this->view->render($response, 'orders/show.twig', [ + 'order' => $order, + 'error' => $e->getMessage(), + ]); + } + } + + public function delete(Response $response, int $id): Response + { + $order = $this->orderService->getOrderById($id); + + if (!$order instanceof Order) { + // Redirect to orders list + return $response->withHeader('Location', '/orders')->withStatus(302); + } + + try { + $this->orderService->deleteOrder($order); + + // Redirect to orders list + return $response->withHeader('Location', '/orders')->withStatus(302); + } catch (InvalidArgumentException $e) { + // Render the order details with error message + return $this->view->render($response, 'orders/show.twig', [ + 'order' => $order, + 'error' => $e->getMessage(), + ]); + } + } +} diff --git a/src/Controller/SettingsController.php b/src/Controller/SettingsController.php new file mode 100644 index 0000000..dfcbc45 --- /dev/null +++ b/src/Controller/SettingsController.php @@ -0,0 +1,74 @@ +configurationService->getAllConfigs() as $setting) { + $settings[$setting->getKey()] = $setting->getValue(); + } + return $this->view->render($response, 'settings/index.twig', [ + 'configs' => $settings, + ]); + } + + public function update(Request $request, Response $response): Response + { + $data = $request->getParsedBody(); + + // Process each configuration value from the form + foreach ($data['configs'] ?? [] as $key => $value) { + try { + // Get existing config or create a new one + $config = $this->configurationService->getConfigByKey($key); + + if ($config instanceof SystemConfig) { + $this->configurationService->updateConfig($config, null, $value); + } else { + $this->configurationService->createConfig($key, $value); + } + } catch (InvalidArgumentException $e) { + // If there's an error, render the form again with error message + return $this->view->render($response, 'settings/index.twig', [ + 'configs' => $this->configurationService->getAllConfigs(), + 'error' => $e->getMessage(), + ]); + } + } + + // Redirect back to settings page with success message + return $response->withHeader('Location', '/settings')->withStatus(302); + } + + public function resetToDefaults(Response $response): Response + { + // Delete all existing configurations + foreach ($this->configurationService->getAllConfigs() as $config) { + $this->configurationService->deleteConfig($config); + } + + // Initialize default configurations + $this->configurationService->initializeDefaultConfigs(); + + // Redirect back to settings page + return $response->withHeader('Location', '/settings')->withStatus(302); + } +} diff --git a/src/Entity/DrinkType.php b/src/Entity/DrinkType.php new file mode 100644 index 0000000..9d272a9 --- /dev/null +++ b/src/Entity/DrinkType.php @@ -0,0 +1,115 @@ +createdAt = $createdAt ?? new DateTimeImmutable(); + $this->updatedAt = $updatedAt ?? new DateTimeImmutable(); + $this->inventoryRecords = new ArrayCollection(); + $this->orderItems = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + $this->updateTimestamp(); + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): self + { + $this->description = $description; + $this->updateTimestamp(); + return $this; + } + + public function getDesiredStock(): int + { + return $this->desiredStock; + } + + public function setDesiredStock(int $desiredStock): self + { + $this->desiredStock = $desiredStock; + $this->updateTimestamp(); + return $this; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeImmutable + { + return $this->updatedAt; + } + + public function getInventoryRecords(): Collection + { + return $this->inventoryRecords; + } + + public function getOrderItems(): Collection + { + return $this->orderItems; + } + + private function updateTimestamp(): void + { + $this->updatedAt = new DateTimeImmutable(); + } +} diff --git a/src/Entity/InventoryRecord.php b/src/Entity/InventoryRecord.php new file mode 100644 index 0000000..bbfcef8 --- /dev/null +++ b/src/Entity/InventoryRecord.php @@ -0,0 +1,99 @@ +timestamp = $timestamp ?? new DateTimeImmutable(); + $this->createdAt = $createdAt ?? new DateTimeImmutable(); + $this->updatedAt = $updatedAt ?? new DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getDrinkType(): DrinkType + { + return $this->drinkType; + } + + public function setDrinkType(DrinkType $drinkType): self + { + $this->drinkType = $drinkType; + $this->updateTimestamp(); + return $this; + } + + public function getQuantity(): int + { + return $this->quantity; + } + + public function setQuantity(int $quantity): self + { + $this->quantity = $quantity; + $this->updateTimestamp(); + return $this; + } + + public function getTimestamp(): DateTimeImmutable + { + return $this->timestamp; + } + + public function setTimestamp(DateTimeImmutable $timestamp): self + { + $this->timestamp = $timestamp; + $this->updateTimestamp(); + return $this; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeImmutable + { + return $this->updatedAt; + } + + private function updateTimestamp(): void + { + $this->updatedAt = new DateTimeImmutable(); + } +} diff --git a/src/Entity/Order.php b/src/Entity/Order.php new file mode 100644 index 0000000..89c363c --- /dev/null +++ b/src/Entity/Order.php @@ -0,0 +1,111 @@ +createdAt = $createdAt ?? new DateTimeImmutable(); + $this->updatedAt = $updatedAt ?? new DateTimeImmutable(); + $this->orderItems = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getStatus(): string + { + return $this->status; + } + + public function setStatus(string $status): self + { + if (!in_array($status, [self::STATUS_NEW, self::STATUS_IN_WORK, self::STATUS_ORDERED, self::STATUS_FULFILLED], true)) { + throw new InvalidArgumentException('Invalid status'); + } + + $this->status = $status; + $this->updateTimestamp(); + return $this; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeImmutable + { + return $this->updatedAt; + } + + /** + * @return Collection + */ + public function getOrderItems(): Collection + { + return $this->orderItems; + } + + public function addOrderItem(OrderItem $orderItem): self + { + if (!$this->orderItems->contains($orderItem)) { + $this->orderItems[] = $orderItem; + $orderItem->setOrder($this); + } + + return $this; + } + + public function removeOrderItem(OrderItem $orderItem): self + { + // set the owning side to null (unless already changed) + if ($this->orderItems->removeElement($orderItem) && $orderItem->getOrder() === $this) { + $orderItem->setOrder(null); + } + + return $this; + } + + private function updateTimestamp(): void + { + $this->updatedAt = new DateTimeImmutable(); + } +} diff --git a/src/Entity/OrderItem.php b/src/Entity/OrderItem.php new file mode 100644 index 0000000..67f5da9 --- /dev/null +++ b/src/Entity/OrderItem.php @@ -0,0 +1,114 @@ +createdAt = $createdAt ?? new DateTimeImmutable(); + $this->updatedAt = $updatedAt ?? new DateTimeImmutable(); + + // Establish bidirectional relationship + if ($this->order instanceof Order) { + $this->order->addOrderItem($this); + } + } + + public function getId(): ?int + { + return $this->id; + } + + public function getOrder(): ?Order + { + return $this->order; + } + + public function setOrder(?Order $order): self + { + // Remove from old order if exists + if ($this->order instanceof Order && $this->order !== $order) { + $this->order->removeOrderItem($this); + } + + // Set new order + $this->order = $order; + + // Add to new order if not null + if ($order instanceof Order && !$order->getOrderItems()->contains($this)) { + $order->addOrderItem($this); + } + + $this->updateTimestamp(); + return $this; + } + + public function getDrinkType(): DrinkType + { + return $this->drinkType; + } + + public function setDrinkType(DrinkType $drinkType): self + { + $this->drinkType = $drinkType; + $this->updateTimestamp(); + return $this; + } + + public function getQuantity(): int + { + return $this->quantity; + } + + public function setQuantity(int $quantity): self + { + $this->quantity = $quantity; + $this->updateTimestamp(); + return $this; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeImmutable + { + return $this->updatedAt; + } + + private function updateTimestamp(): void + { + $this->updatedAt = new DateTimeImmutable(); + } +} diff --git a/src/Entity/SystemConfig.php b/src/Entity/SystemConfig.php new file mode 100644 index 0000000..66a007f --- /dev/null +++ b/src/Entity/SystemConfig.php @@ -0,0 +1,81 @@ +createdAt = $createdAt ?? new DateTimeImmutable(); + $this->updatedAt = $updatedAt ?? new DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getKey(): string + { + return $this->key; + } + + public function setKey(string $key): self + { + $this->key = $key; + $this->updateTimestamp(); + return $this; + } + + public function getValue(): string + { + return $this->value; + } + + public function setValue(string $value): self + { + $this->value = $value; + $this->updateTimestamp(); + return $this; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeImmutable + { + return $this->updatedAt; + } + + private function updateTimestamp(): void + { + $this->updatedAt = new DateTimeImmutable(); + } +} diff --git a/src/Enum/SystemSettingKey.php b/src/Enum/SystemSettingKey.php new file mode 100644 index 0000000..476600c --- /dev/null +++ b/src/Enum/SystemSettingKey.php @@ -0,0 +1,27 @@ +handle($request); + } catch (HttpException $e) { + return $this->handleException($e, $request, $e->getCode()); + } catch (Throwable $e) { + return $this->handleException($e, $request); + } + } + + private function handleException(Throwable $e, ServerRequestInterface $request, int $statusCode = 500): ResponseInterface + { + $this->logger->error($e->getMessage(), [ + 'exception' => $e, + 'request' => [ + 'method' => $request->getMethod(), + 'uri' => (string) $request->getUri(), + ], + ]); + + $response = new Response($statusCode); + + // Only show error details in development + $error = [ + 'message' => $this->displayErrorDetails ? $e->getMessage() : 'An error occurred', + ]; + + if ($this->displayErrorDetails) { + $error['trace'] = $e->getTraceAsString(); + $error['file'] = $e->getFile(); + $error['line'] = $e->getLine(); + } + + // Try to render an error page, fall back to JSON if Twig fails + try { + return $this->twig->render($response, 'error.twig', [ + 'error' => $error, + 'status_code' => $statusCode, + ]); + } catch (Throwable $e) { + $this->logger->error('Failed to render error template', ['exception' => $e]); + + $response = $response->withHeader('Content-Type', 'application/json'); + $response->getBody()->write(json_encode(['error' => $error])); + + return $response; + } + } +} diff --git a/src/Repository/AbstractRepository.php b/src/Repository/AbstractRepository.php new file mode 100644 index 0000000..db1e3f6 --- /dev/null +++ b/src/Repository/AbstractRepository.php @@ -0,0 +1,33 @@ + + */ +abstract class AbstractRepository extends EntityRepository +{ + /** + * @param E $entity + * @return void + */ + public function save(object $entity): void + { + $this->getEntityManager()->persist($entity); + $this->getEntityManager()->flush(); + } + + /** + * @param E $entity + */ + public function remove(object $entity): void + { + $this->getEntityManager()->remove($entity); + $this->getEntityManager()->flush(); + } +} diff --git a/src/Repository/DrinkTypeRepository.php b/src/Repository/DrinkTypeRepository.php new file mode 100644 index 0000000..7571528 --- /dev/null +++ b/src/Repository/DrinkTypeRepository.php @@ -0,0 +1,20 @@ + + */ +class DrinkTypeRepository extends AbstractRepository +{ + public function __construct( + EntityManagerInterface $entityManager + ) { + parent::__construct($entityManager, $entityManager->getClassMetadata(DrinkType::class)); + } +} diff --git a/src/Repository/InventoryRecordRepository.php b/src/Repository/InventoryRecordRepository.php new file mode 100644 index 0000000..21cf1f2 --- /dev/null +++ b/src/Repository/InventoryRecordRepository.php @@ -0,0 +1,61 @@ + + */ +class InventoryRecordRepository extends AbstractRepository +{ + public function __construct( + EntityManagerInterface $entityManager + ) { + parent::__construct($entityManager, $entityManager->getClassMetadata(InventoryRecord::class)); + } + + /** + * @return array + */ + public function findByDrinkType(DrinkType $drinkType): array + { + return $this->findBy(['drinkType' => $drinkType], ['timestamp' => 'DESC']); + } + + public function findLatestByDrinkType(DrinkType $drinkType): ?InventoryRecord + { + $records = $this->findBy( + ['drinkType' => $drinkType], + ['timestamp' => 'DESC'], + 1 + ); + + return $records[0] ?? null; + } + + /** + * @return array + */ + public function findByTimestampRange(DateTimeImmutable $start, DateTimeImmutable $end): array + { + $qb = $this->getEntityManager()->createQueryBuilder(); + $qb->select('ir') + ->from(InventoryRecord::class, 'ir') + ->where('ir.timestamp >= :start') + ->andWhere('ir.timestamp <= :end') + ->setParameter('start', $start) + ->setParameter('end', $end) + ->orderBy('ir.timestamp', 'DESC'); + + /** @var array $result */ + $result = $qb->getQuery()->getResult(); + return $result; + } + +} diff --git a/src/Repository/OrderItemRepository.php b/src/Repository/OrderItemRepository.php new file mode 100644 index 0000000..29a8291 --- /dev/null +++ b/src/Repository/OrderItemRepository.php @@ -0,0 +1,46 @@ + + */ +class OrderItemRepository extends AbstractRepository +{ + public function __construct( + EntityManagerInterface $entityManager + ) { + parent::__construct($entityManager, $entityManager->getClassMetadata(OrderItem::class)); + } + + /** + * @return array + */ + public function findByOrder(Order $order): array + { + return $this->findBy(['order' => $order]); + } + + /** + * @return array + */ + public function findByDrinkType(DrinkType $drinkType): array + { + return $this->findBy(['drinkType' => $drinkType]); + } + + public function findByOrderAndDrinkType(Order $order, DrinkType $drinkType): ?OrderItem + { + return $this->findOneBy([ + 'order' => $order, + 'drinkType' => $drinkType, + ]); + } +} diff --git a/src/Repository/OrderRepository.php b/src/Repository/OrderRepository.php new file mode 100644 index 0000000..6c8abf8 --- /dev/null +++ b/src/Repository/OrderRepository.php @@ -0,0 +1,74 @@ + + */ +class OrderRepository extends AbstractRepository +{ + public function __construct( + EntityManagerInterface $entityManager + ) { + parent::__construct($entityManager, $entityManager->getClassMetadata(Order::class)); + } + + /** + * @return array + */ + public function findByStatus(string $status): array + { + return $this->findBy(['status' => $status], ['createdAt' => 'DESC']); + } + + /** + * @return array + */ + public function findByDateRange(DateTimeImmutable $start, DateTimeImmutable $end): array + { + $qb = $this->getEntityManager()->createQueryBuilder(); + $qb->select('o') + ->from(Order::class, 'o') + ->where('o.createdAt >= :start') + ->andWhere('o.createdAt <= :end') + ->setParameter('start', $start) + ->setParameter('end', $end) + ->orderBy('o.createdAt', 'DESC'); + + /** @var array $result */ + $result = $qb->getQuery()->getResult(); + return $result; + } + + /** + * Find the last N orders that contain a specific drink type + * + * @param DrinkType $drinkType The drink type to search for + * @param int $limit The maximum number of orders to return + * @return array The last N orders containing the drink type, ordered by creation date (newest first) + */ + public function findLastOrdersForDrinkType(DrinkType $drinkType, int $limit = 5): array + { + $qb = $this->getEntityManager()->createQueryBuilder(); + $qb->select('o') + ->from(Order::class, 'o') + ->join('o.orderItems', 'oi') + ->where('oi.drinkType = :drinkType') + ->andWhere('o.status = :status') // Only consider fulfilled orders + ->setParameter('drinkType', $drinkType) + ->setParameter('status', Order::STATUS_FULFILLED) + ->orderBy('o.createdAt', 'DESC') + ->setMaxResults($limit); + + /** @var array $result */ + $result = $qb->getQuery()->getResult(); + return $result; + } +} diff --git a/src/Repository/SystemConfigRepository.php b/src/Repository/SystemConfigRepository.php new file mode 100644 index 0000000..3d557a6 --- /dev/null +++ b/src/Repository/SystemConfigRepository.php @@ -0,0 +1,43 @@ + + */ +class SystemConfigRepository extends AbstractRepository +{ + public function __construct( + EntityManagerInterface $entityManager + ) { + parent::__construct($entityManager, $entityManager->getClassMetadata(SystemConfig::class)); + } + + public function findByKey(string $key): ?SystemConfig + { + return $this->findOneBy(['key' => $key]); + } + + public function getValue(string $key, string $default = ''): string + { + $config = $this->findByKey($key); + return $config instanceof SystemConfig ? $config->getValue() : $default; + } + + public function setValue(string $key, string $value): void + { + $config = $this->findByKey($key); + + if ($config instanceof SystemConfig) { + $config->setValue($value); + } else { + $config = new SystemConfig($key, $value); + } + $this->save($config); + } +} diff --git a/src/Service/ConfigurationService.php b/src/Service/ConfigurationService.php new file mode 100644 index 0000000..fff75a6 --- /dev/null +++ b/src/Service/ConfigurationService.php @@ -0,0 +1,197 @@ +systemConfigRepository->findAll(); + } + + /** + * Get a configuration value by key + * + * @param string $key + * @param string $default Default value if the key doesn't exist + * @return string + */ + public function getConfigValue(string $key, string $default = ''): string + { + return $this->systemConfigRepository->getValue($key, $default); + } + + /** + * Set a configuration value + * + * @param string $key + * @param string $value + * @return void + */ + public function setConfigValue(string $key, string $value): void + { + $this->systemConfigRepository->setValue($key, $value); + } + + /** + * Get a configuration entity by key + * + * @param string $key + * @return SystemConfig|null + */ + public function getConfigByKey(string $key): ?SystemConfig + { + return $this->systemConfigRepository->findByKey($key); + } + + /** + * Create a new configuration entry + * + * @param string $key + * @param string $value + * @return SystemConfig + * @throws InvalidArgumentException If a configuration with the same key already exists + */ + public function createConfig(string $key, string $value): SystemConfig + { + if ($this->systemConfigRepository->findByKey($key) instanceof SystemConfig) { + throw new InvalidArgumentException("A configuration with the key '$key' already exists"); + } + + $config = new SystemConfig($key, $value); + $this->systemConfigRepository->save($config); + + return $config; + } + + /** + * Update an existing configuration entry + * + * @param SystemConfig $config + * @param string|null $key + * @param string|null $value + * @return SystemConfig + * @throws InvalidArgumentException If a configuration with the same key already exists + */ + public function updateConfig( + SystemConfig $config, + ?string $key = null, + ?string $value = null + ): SystemConfig { + // Update key if provided + if ($key !== null && $key !== $config->getKey()) { + // Check if a configuration with the same key already exists + if ($this->systemConfigRepository->findByKey($key) instanceof SystemConfig) { + throw new InvalidArgumentException("A configuration with the key '$key' already exists"); + } + $config->setKey($key); + } + + // Update value if provided + if ($value !== null) { + $config->setValue($value); + } + + $this->systemConfigRepository->save($config); + + return $config; + } + + /** + * Delete a configuration entry + * + * @param SystemConfig $config + * @return void + */ + public function deleteConfig(SystemConfig $config): void + { + $this->systemConfigRepository->remove($config); + } + + /** + * Initialize default configuration values if they don't exist + * + * @return void + */ + public function initializeDefaultConfigs(): void + { + $defaults = [ + SystemSettingKey::STOCK_ADJUSTMENT_LOOKBACK->value => '30', // Deprecated + SystemSettingKey::STOCK_ADJUSTMENT_LOOKBACK_ORDERS->value => '5', + SystemSettingKey::STOCK_ADJUSTMENT_MAGNITUDE->value => '0.2', + SystemSettingKey::STOCK_ADJUSTMENT_THRESHOLD->value => '0.1', + ]; + + foreach ($defaults as $key => $value) { + if (!$this->getConfigByKey($key) instanceof SystemConfig) { + $this->createConfig($key, $value); + } + } + } +} diff --git a/src/Service/DrinkTypeService.php b/src/Service/DrinkTypeService.php new file mode 100644 index 0000000..1c09851 --- /dev/null +++ b/src/Service/DrinkTypeService.php @@ -0,0 +1,128 @@ +drinkTypeRepository->findAll(); + } + + /** + * Get a drink type by ID + * + * @param int $id + * @return DrinkType|null + */ + public function getDrinkTypeById(int $id): ?DrinkType + { + return $this->drinkTypeRepository->find($id); + } + + /** + * Get a drink type by name + * + * @param string $name + * @return DrinkType|null + */ + public function getDrinkTypeByName(string $name): ?DrinkType + { + return $this->drinkTypeRepository->findOneBy(['name' => $name]); + } + + /** + * Create a new drink type + * + * @param string $name + * @param string|null $description + * @param int|null $desiredStock + * @return DrinkType + * @throws InvalidArgumentException If a drink type with the same name already exists + */ + public function createDrinkType(string $name, ?string $description = null, ?int $desiredStock = null): DrinkType + { + // Check if a drink type with the same name already exists + if ($this->drinkTypeRepository->findOneBy(['name' => $name]) !== null) { + throw new InvalidArgumentException("A drink type with the name '$name' already exists"); + } + + // If no desired stock is provided, use the default from configuration + if ($desiredStock === null) { + $desiredStock = (int) $this->configService->getConfigValue('default_desired_stock', '10'); + } + + $drinkType = new DrinkType($name, $description, $desiredStock); + $this->drinkTypeRepository->save($drinkType); + + return $drinkType; + } + + /** + * Update an existing drink type + * + * @param DrinkType $drinkType + * @param string|null $name + * @param string|null $description + * @param int|null $desiredStock + * @return DrinkType + * @throws InvalidArgumentException If a drink type with the same name already exists + */ + public function updateDrinkType( + DrinkType $drinkType, + ?string $name = null, + ?string $description = null, + ?int $desiredStock = null + ): DrinkType { + // Update name if provided + if ($name !== null && $name !== $drinkType->getName()) { + // Check if a drink type with the same name already exists + if ($this->drinkTypeRepository->findOneBy(['name' => $name]) !== null) { + throw new InvalidArgumentException("A drink type with the name '$name' already exists"); + } + $drinkType->setName($name); + } + + // Update description if provided + if ($description !== null) { + $drinkType->setDescription($description); + } + + // Update desired stock if provided + if ($desiredStock !== null) { + $drinkType->setDesiredStock($desiredStock); + } + + $this->drinkTypeRepository->save($drinkType); + + return $drinkType; + } + + /** + * Delete a drink type + * + * @param DrinkType $drinkType + * @return void + */ + public function deleteDrinkType(DrinkType $drinkType): void + { + $this->drinkTypeRepository->remove($drinkType); + } +} diff --git a/src/Service/InventoryService.php b/src/Service/InventoryService.php new file mode 100644 index 0000000..d55fd80 --- /dev/null +++ b/src/Service/InventoryService.php @@ -0,0 +1,204 @@ +inventoryRecordRepository->findAll(); + } + + /** + * Get inventory records for a specific drink type + * + * @param DrinkType $drinkType + * @return InventoryRecord[] + */ + public function getInventoryRecordsByDrinkType(DrinkType $drinkType): array + { + return $this->inventoryRecordRepository->findByDrinkType($drinkType); + } + + /** + * Get the latest inventory record for a specific drink type + * + * @param DrinkType $drinkType + * @return InventoryRecord|null + */ + public function getLatestInventoryRecord(DrinkType $drinkType): ?InventoryRecord + { + return $this->inventoryRecordRepository->findLatestByDrinkType($drinkType); + } + + /** + * Get inventory records within a specific time range + * + * @param DateTimeImmutable $start + * @param DateTimeImmutable $end + * @return InventoryRecord[] + */ + public function getInventoryRecordsByTimeRange(DateTimeImmutable $start, DateTimeImmutable $end): array + { + return $this->inventoryRecordRepository->findByTimestampRange($start, $end); + } + + /** + * Get the current stock level for a specific drink type + * + * @param DrinkType $drinkType + * @return int + */ + public function getCurrentStockLevel(DrinkType $drinkType): int + { + $latestRecord = $this->getLatestInventoryRecord($drinkType); + return $latestRecord instanceof InventoryRecord ? $latestRecord->getQuantity() : 0; + } + + /** + * Get the difference between current and desired stock for a specific drink type + * + * @param DrinkType $drinkType + * @return int Positive value means excess stock, negative value means shortage + */ + public function getStockDifference(DrinkType $drinkType): int + { + $currentStock = $this->getCurrentStockLevel($drinkType); + $desiredStock = $drinkType->getDesiredStock(); + + return $currentStock - $desiredStock; + } + + /** + * Update the stock level for a specific drink type + * + * @param DrinkType $drinkType + * @param int $quantity + * @param DateTimeImmutable|null $timestamp + * @return InventoryRecord + */ + public function updateStockLevel( + DrinkType $drinkType, + int $quantity, + ?DateTimeImmutable $timestamp = null + ): InventoryRecord { + $inventoryRecord = new InventoryRecord( + $drinkType, + $quantity, + $timestamp + ); + + $this->inventoryRecordRepository->save($inventoryRecord); + + return $inventoryRecord; + } + + /** + * Get all drink types with their current stock levels + * + * @return array + */ + public function getAllDrinkTypesWithStockLevels(): array + { + $drinkTypes = $this->drinkTypeRepository->findAll(); + $result = []; + + foreach ($drinkTypes as $drinkType) { + $currentStock = $this->getCurrentStockLevel($drinkType); + $desiredStock = $drinkType->getDesiredStock(); + $difference = $currentStock - $desiredStock; + + $result[] = [ + 'drinkType' => $drinkType, + 'currentStock' => $currentStock, + 'desiredStock' => $desiredStock, + 'difference' => $difference, + ]; + } + + return $result; + } + + /** + * Get all drink types with low stock (current stock below low stock threshold) + * + * @return array + */ + public function getLowStockDrinkTypes(): array + { + $lowStockThreshold = (float) $this->configService->getConfigValue(SystemSettingKey::LOW_STOCK_THRESHOLD->value, '50') / 100; + $drinkTypes = $this->drinkTypeRepository->findAll(); + $result = []; + + foreach ($drinkTypes as $drinkType) { + $currentStock = $this->getCurrentStockLevel($drinkType); + $desiredStock = $drinkType->getDesiredStock(); + + // Check if current stock is below the threshold percentage of desired stock + if ($desiredStock > 0 && $currentStock < ($desiredStock * $lowStockThreshold)) { + $shortage = $desiredStock - $currentStock; + + $result[] = [ + 'drinkType' => $drinkType, + 'currentStock' => $currentStock, + 'desiredStock' => $desiredStock, + 'shortage' => $shortage, + ]; + } + } + + return $result; + } + + /** + * Get all drink types with critical stock (current stock below critical stock threshold) + * + * @return array + */ + public function getCriticalStockDrinkTypes(): array + { + $criticalStockThreshold = (float) $this->configService->getConfigValue(SystemSettingKey::CRITICAL_STOCK_THRESHOLD->value, '25') / 100; + $drinkTypes = $this->drinkTypeRepository->findAll(); + $result = []; + + foreach ($drinkTypes as $drinkType) { + $currentStock = $this->getCurrentStockLevel($drinkType); + $desiredStock = $drinkType->getDesiredStock(); + + // Check if current stock is below the threshold percentage of desired stock + if ($desiredStock > 0 && $currentStock < ($desiredStock * $criticalStockThreshold)) { + $shortage = $desiredStock - $currentStock; + + $result[] = [ + 'drinkType' => $drinkType, + 'currentStock' => $currentStock, + 'desiredStock' => $desiredStock, + 'shortage' => $shortage, + ]; + } + } + + return $result; + } +} diff --git a/src/Service/OrderService.php b/src/Service/OrderService.php new file mode 100644 index 0000000..f645097 --- /dev/null +++ b/src/Service/OrderService.php @@ -0,0 +1,217 @@ +orderRepository->findAll(); + } + + /** + * Get an order by ID + * + * @param int $id + * @return Order|null + */ + public function getOrderById(int $id): ?Order + { + return $this->orderRepository->find($id); + } + + /** + * Get orders by status + * + * @param string $status + * @return Order[] + */ + public function getOrdersByStatus(string $status): array + { + return $this->orderRepository->findByStatus($status); + } + + /** + * Get orders within a specific date range + * + * @param DateTimeImmutable $start + * @param DateTimeImmutable $end + * @return Order[] + */ + public function getOrdersByDateRange(DateTimeImmutable $start, DateTimeImmutable $end): array + { + return $this->orderRepository->findByDateRange($start, $end); + } + + /** + * Create a new order + * + * @param array $items + * @return Order + * @throws InvalidArgumentException If a drink type ID is invalid + */ + public function createOrder(array $items): Order + { + $order = new Order(); + $this->orderRepository->save($order); + + foreach ($items as $item) { + $drinkType = $this->drinkTypeRepository->find($item['drinkTypeId']); + if ($drinkType === null) { + throw new InvalidArgumentException("Invalid drink type ID: {$item['drinkTypeId']}"); + } + + $orderItem = new OrderItem($drinkType, $item['quantity'], $order); + $this->orderItemRepository->save($orderItem); + } + + return $order; + } + + /** + * Create an order based on current stock levels + * + * @return Order + */ + public function createOrderFromStockLevels(): Order + { + $lowStockItems = $this->inventoryService->getAllDrinkTypesWithStockLevels(); + $orderItems = []; + + foreach ($lowStockItems as $item) { + if ($item['currentStock'] < $item['desiredStock']) { + $orderItems[] = [ + 'drinkTypeId' => $item['drinkType']->getId(), + 'quantity' => $item['desiredStock'] - $item['currentStock'], + ]; + } + } + + return $this->createOrder($orderItems); + } + + /** + * Update an order's status + * + * @param Order $order + * @param string $status + * @return Order + * @throws InvalidArgumentException If the status is invalid + */ + public function updateOrderStatus(Order $order, string $status): Order + { + $order->setStatus($status); + $this->orderRepository->save($order); + + // If the order is fulfilled, update the inventory + if ($status === Order::STATUS_FULFILLED) { + $this->updateInventoryFromOrder($order); + } + + return $order; + } + + /** + * Update inventory based on a fulfilled order + * + * @param Order $order + * @return void + */ + private function updateInventoryFromOrder(Order $order): void + { + foreach ($order->getOrderItems() as $orderItem) { + $drinkType = $orderItem->getDrinkType(); + $currentStock = $this->inventoryService->getCurrentStockLevel($drinkType); + $newStock = $currentStock + $orderItem->getQuantity(); + + $this->inventoryService->updateStockLevel($drinkType, $newStock); + } + } + + /** + * Add an item to an order + * + * @param Order $order + * @param DrinkType $drinkType + * @param int $quantity + * @return OrderItem + * @throws InvalidArgumentException If the order is not in 'new' or 'in_work' status + */ + public function addOrderItem(Order $order, DrinkType $drinkType, int $quantity): OrderItem + { + if (!in_array($order->getStatus(), [Order::STATUS_NEW, Order::STATUS_IN_WORK], true)) { + throw new InvalidArgumentException("Cannot add items to an order with status '{$order->getStatus()}'"); + } + + // Check if the order already has an item for this drink type + $existingItem = $this->orderItemRepository->findByOrderAndDrinkType($order, $drinkType); + + if ($existingItem instanceof OrderItem) { + // Update the existing item + $existingItem->setQuantity($existingItem->getQuantity() + $quantity); + $this->orderItemRepository->save($existingItem); + return $existingItem; + } + // Create a new item + $orderItem = new OrderItem($drinkType, $quantity, $order); + $this->orderItemRepository->save($orderItem); + return $orderItem; + } + + /** + * Remove an item from an order + * + * @param Order $order + * @param OrderItem $orderItem + * @return void + * @throws InvalidArgumentException If the order is not in 'new' or 'in_work' status + */ + public function removeOrderItem(Order $order, OrderItem $orderItem): void + { + if (!in_array($order->getStatus(), [Order::STATUS_NEW, Order::STATUS_IN_WORK], true)) { + throw new InvalidArgumentException("Cannot remove items from an order with status '{$order->getStatus()}'"); + } + + $order->removeOrderItem($orderItem); + $this->orderItemRepository->remove($orderItem); + } + + /** + * Delete an order + * + * @param Order $order + * @return void + * @throws InvalidArgumentException If the order is not in 'new' status + */ + public function deleteOrder(Order $order): void + { + if ($order->getStatus() !== Order::STATUS_NEW) { + throw new InvalidArgumentException("Cannot delete an order with status '{$order->getStatus()}'"); + } + + $this->orderRepository->remove($order); + } +} diff --git a/src/Service/StockAdjustmentService.php b/src/Service/StockAdjustmentService.php new file mode 100644 index 0000000..ced9a8f --- /dev/null +++ b/src/Service/StockAdjustmentService.php @@ -0,0 +1,262 @@ + + */ + public function adjustStockLevels(): array + { + $lookbackOrders = (int) $this->configService->getConfigValue( + 'stock_adjustment_lookback_orders', + (string) self::DEFAULT_LOOKBACK_ORDERS + ); + + $adjustmentMagnitude = (float) $this->configService->getConfigValue( + 'stock_adjustment_magnitude', + (string) self::DEFAULT_ADJUSTMENT_MAGNITUDE + ); + + $adjustmentThreshold = (float) $this->configService->getConfigValue( + 'stock_adjustment_threshold', + (string) self::DEFAULT_ADJUSTMENT_THRESHOLD + ); + + $drinkTypes = $this->drinkTypeRepository->findAll(); + $result = []; + + foreach ($drinkTypes as $drinkType) { + $consumptionRate = $this->calculateConsumptionRateFromOrders($drinkType, $lookbackOrders); + + if ($consumptionRate === null) { + continue; // Skip if we don't have enough data + } + + $currentDesiredStock = $drinkType->getDesiredStock(); + // For the suggested stock, we use the consumption rate directly as it's already weighted + $suggestedStock = (int) ceil($consumptionRate); + + // Only adjust if the difference is significant + $difference = abs($suggestedStock - $currentDesiredStock) / max(1, $currentDesiredStock); + + if ($difference >= $adjustmentThreshold) { + // Calculate the new desired stock with a gradual adjustment + $adjustment = (int) ceil(($suggestedStock - $currentDesiredStock) * $adjustmentMagnitude); + $newDesiredStock = $currentDesiredStock + $adjustment; + + // Ensure we don't go below 1 + $newDesiredStock = max(1, $newDesiredStock); + + // Update the drink type + $drinkType->setDesiredStock($newDesiredStock); + $this->drinkTypeRepository->save($drinkType); + + $result[] = [ + 'drinkType' => $drinkType, + 'oldDesiredStock' => $currentDesiredStock, + 'newDesiredStock' => $newDesiredStock, + 'adjustmentReason' => $suggestedStock > $currentDesiredStock + ? 'Increased consumption rate' + : 'Decreased consumption rate', + ]; + } + } + + return $result; + } + + /** + * Calculate the weighted consumption rate based on the last N orders for a drink type + * + * @param DrinkType $drinkType + * @param int $lookbackOrders Number of past orders to consider + * @return float|null Weighted consumption rate, or null if not enough data + */ + private function calculateConsumptionRateFromOrders( + DrinkType $drinkType, + int $lookbackOrders + ): ?float { + // Get the last N orders for this drink type + $orders = $this->orderRepository->findLastOrdersForDrinkType($drinkType, $lookbackOrders); + + // Need at least one order to calculate consumption + if (count($orders) === 0) { + return null; + } + + // Calculate weighted consumption + $totalWeightedConsumption = 0; + $totalWeight = 0; + + // Define weights - most recent order has highest weight + // For example, with 5 orders: weights are 5, 4, 3, 2, 1 + $weights = range($lookbackOrders, 1); + + foreach ($orders as $index => $order) { + // Get the order items for this drink type + $orderItems = $order->getOrderItems(); + $quantity = 0; + + // Sum up quantities for this drink type + foreach ($orderItems as $item) { + if ($item->getDrinkType()->getId() === $drinkType->getId()) { + $quantity += $item->getQuantity(); + } + } + + // Apply weight to this order's consumption + $weight = $weights[$index] ?? 1; // Fallback to weight 1 if index is out of bounds + $totalWeightedConsumption += $quantity * $weight; + $totalWeight += $weight; + } + + // If total weight is 0, return null (shouldn't happen, but just in case) + if ($totalWeight === 0) { + return null; + } + + // Calculate weighted average consumption + return $totalWeightedConsumption / $totalWeight; + } + + /** + * Calculate the average daily consumption rate for a drink type + * + * @deprecated Use calculateConsumptionRateFromOrders instead + * @param DrinkType $drinkType + * @param DateTimeImmutable $startDate + * @param DateTimeImmutable $endDate + * @return float|null Average daily consumption rate, or null if not enough data + */ + private function calculateConsumptionRate( + DrinkType $drinkType, + DateTimeImmutable $startDate, + DateTimeImmutable $endDate + ): ?float { + // Get inventory records for the period + $inventoryRecords = $this->inventoryRecordRepository->findByDrinkType($drinkType); + + // Filter records within the date range + $inventoryRecords = array_filter($inventoryRecords, function ($record) use ($startDate, $endDate): bool { + $timestamp = $record->getTimestamp(); + return $timestamp >= $startDate && $timestamp <= $endDate; + }); + + // Sort by timestamp + usort($inventoryRecords, fn($a, $b): int => $a->getTimestamp() <=> $b->getTimestamp()); + + // Need at least two records to calculate consumption + if (count($inventoryRecords) < 2) { + return null; + } + + // Calculate total consumption + $totalConsumption = 0; + $previousRecord = null; + + foreach ($inventoryRecords as $record) { + if ($previousRecord instanceof InventoryRecord) { + $previousQuantity = $previousRecord->getQuantity(); + $currentQuantity = $record->getQuantity(); + $daysDifference = $record->getTimestamp()->diff($previousRecord->getTimestamp())->days; + + // If days difference is 0, use 1 to avoid division by zero + $daysDifference = max(1, $daysDifference); + + // If current quantity is less than previous, consumption occurred + if ($currentQuantity < $previousQuantity) { + $consumption = $previousQuantity - $currentQuantity; + $totalConsumption += $consumption; + } + } + + $previousRecord = $record; + } + + // Calculate days between first and last record + $firstRecord = reset($inventoryRecords); + $lastRecord = end($inventoryRecords); + $totalDays = $lastRecord->getTimestamp()->diff($firstRecord->getTimestamp())->days; + + // If total days is 0, use 1 to avoid division by zero + $totalDays = max(1, $totalDays); + + // Calculate average daily consumption + return $totalConsumption / $totalDays; + } + + /** + * Get the consumption history for a drink type + * + * @param DrinkType $drinkType + * @param DateTimeImmutable $startDate + * @param DateTimeImmutable $endDate + * @return array + */ + public function getConsumptionHistory( + DrinkType $drinkType, + DateTimeImmutable $startDate, + DateTimeImmutable $endDate + ): array { + // Get inventory records for the period + $inventoryRecords = $this->inventoryRecordRepository->findByDrinkType($drinkType); + + // Filter records within the date range + $inventoryRecords = array_filter($inventoryRecords, function ($record) use ($startDate, $endDate): bool { + $timestamp = $record->getTimestamp(); + return $timestamp >= $startDate && $timestamp <= $endDate; + }); + + // Sort by timestamp + usort($inventoryRecords, fn($a, $b): int => $a->getTimestamp() <=> $b->getTimestamp()); + + // Calculate consumption between each record + $consumptionHistory = []; + $previousRecord = null; + + foreach ($inventoryRecords as $record) { + if ($previousRecord !== null) { + $previousQuantity = $previousRecord->getQuantity(); + $currentQuantity = $record->getQuantity(); + + // If current quantity is less than previous, consumption occurred + if ($currentQuantity < $previousQuantity) { + $consumption = $previousQuantity - $currentQuantity; + $consumptionHistory[] = [ + 'date' => $record->getTimestamp(), + 'consumption' => $consumption, + ]; + } + } + + $previousRecord = $record; + } + + return $consumptionHistory; + } +} diff --git a/src/Settings.php b/src/Settings.php new file mode 100644 index 0000000..b2e8c9c --- /dev/null +++ b/src/Settings.php @@ -0,0 +1,14 @@ +get(\App\Settings::class); + + // create cache dirs + foreach (['twig', 'doctrine/proxy'] as $dir) { + if (!is_dir($settings->cacheDir . '/' . $dir)) { + mkdir($settings->cacheDir . '/' . $dir, 0777, true); + } + } + + // Create app with container + $app = Bridge::create($container); + + // Add middleware + $app->add(new ErrorHandlerMiddleware( + $container->get(Logger::class), + $container->get(Twig::class), + true // Set to false in production + )); + + // Add routes + (require __DIR__ . '/routes.php')($app); + + return $app; +})(); diff --git a/src/routes.php b/src/routes.php new file mode 100644 index 0000000..33947a2 --- /dev/null +++ b/src/routes.php @@ -0,0 +1,48 @@ +get('/', [DashboardController::class, 'index']); + + // Drink Type routes + $app->get('/drink-types', [DrinkTypeController::class, 'index']); + $app->get('/drink-types/create', [DrinkTypeController::class, 'create']); + $app->post('/drink-types', [DrinkTypeController::class, 'store']); + $app->get('/drink-types/{id:[0-9]+}', [DrinkTypeController::class, 'show']); + $app->get('/drink-types/{id:[0-9]+}/edit', [DrinkTypeController::class, 'edit']); + $app->post('/drink-types/{id:[0-9]+}', [DrinkTypeController::class, 'update']); + $app->post('/drink-types/{id:[0-9]+}/delete', [DrinkTypeController::class, 'delete']); + + // Inventory routes + $app->get('/inventory', [InventoryController::class, 'index']); + $app->get('/inventory/update', [InventoryController::class, 'showUpdateForm']); + $app->post('/inventory/update', [InventoryController::class, 'update']); + $app->get('/inventory/history', [InventoryController::class, 'history']); + $app->get('/inventory/history/{id:[0-9]+}', [InventoryController::class, 'history']); + $app->get('/inventory/low-stock', [InventoryController::class, 'lowStock']); + + // Order routes + $app->get('/orders', [OrderController::class, 'index']); + $app->get('/orders/create', [OrderController::class, 'create']); + $app->post('/orders', [OrderController::class, 'store']); + $app->get('/orders/create-from-stock', [OrderController::class, 'createFromStock']); + $app->get('/orders/{id:[0-9]+}', [OrderController::class, 'show']); + $app->post('/orders/{id:[0-9]+}/status', [OrderController::class, 'updateStatus']); + $app->post('/orders/{id:[0-9]+}/items', [OrderController::class, 'addItem']); + $app->post('/orders/{id:[0-9]+}/items/{itemId:[0-9]+}/remove', [OrderController::class, 'removeItem']); + $app->post('/orders/{id:[0-9]+}/delete', [OrderController::class, 'delete']); + + // Settings routes + $app->get('/settings', [SettingsController::class, 'index']); + $app->post('/settings', [SettingsController::class, 'update']); + $app->post('/settings/reset', [SettingsController::class, 'resetToDefaults']); +}; diff --git a/templates/components/flash-messages.twig b/templates/components/flash-messages.twig new file mode 100644 index 0000000..1bab07f --- /dev/null +++ b/templates/components/flash-messages.twig @@ -0,0 +1,21 @@ +{% if flash %} +
+ {% for type, messages in flash %} + {% for message in messages %} +
+ {% if type == 'success' %} + 🎉🎊 SUCCESS! 🎊🎉 {{ message }} 🥳🙌 WOOHOO! 🌟✨ + {% elseif type == 'error' %} + ❌😱 ERROR! 😱❌ {{ message }} 💥😭 OH NO! 💔🔥 + {% elseif type == 'warning' %} + ⚠️😬 WARNING! 😬⚠️ {{ message }} 🚨🚧 BE CAREFUL! 🚧🚨 + {% elseif type == 'info' %} + ℹ️🧐 INFO! 🧐ℹ️ {{ message }} 📝💡 GOOD TO KNOW! 💭📊 + {% else %} + 💌✨ {{ message }} ✨💌 + {% endif %} +
+ {% endfor %} + {% endfor %} +
+{% endif %} diff --git a/templates/components/form.twig b/templates/components/form.twig new file mode 100644 index 0000000..64dab11 --- /dev/null +++ b/templates/components/form.twig @@ -0,0 +1,73 @@ +{# 🌈✨ Super Amazing Form Components 🎨🎉 #} + +{# 📝 Input Field 📝 #} +{% macro input(name, label, value, type = 'text', required = false, placeholder = '', class = '') %} +
+ + +
+{% endmacro %} + +{# 📄 Textarea Field 📄 #} +{% macro textarea(name, label, value, required = false, rows = 3, placeholder = '', class = '') %} +
+ + +
+{% endmacro %} + +{# 📋 Select Field 📋 #} +{% macro select(name, label, options, selected, required = false, class = '') %} +
+ + +
+{% endmacro %} + +{# ✅ Checkbox Field ✅ #} +{% macro checkbox(name, label, checked = false, value = '1', class = '') %} +
+ + +
+{% endmacro %} + +{# 🔘 Radio Button 🔘 #} +{% macro radio(name, label, value, checked = false, class = '') %} +
+ + +
+{% endmacro %} + +{# 🚀 Submit Button 🚀 #} +{% macro submit(label = 'Submit', class = 'btn-primary') %} + +{% endmacro %} + +{# 🔳 Button 🔳 #} +{% macro button(label, type = 'button', class = 'btn-secondary', attributes = '') %} + +{% endmacro %} + +{# ⚠️ Form Error Display ⚠️ #} +{% macro errors(error) %} + {% if error %} +
+ ⚠️😱 ERROR ALERT! 😱⚠️ {{ error }} 💥💔 +
+ {% endif %} +{% endmacro %} diff --git a/templates/components/navigation.twig b/templates/components/navigation.twig new file mode 100644 index 0000000..a3c21a3 --- /dev/null +++ b/templates/components/navigation.twig @@ -0,0 +1,9 @@ + diff --git a/templates/dashboard/index.twig b/templates/dashboard/index.twig new file mode 100644 index 0000000..2409202 --- /dev/null +++ b/templates/dashboard/index.twig @@ -0,0 +1,145 @@ +{% extends "layout.twig" %} + +{% block title %}🏠 Dashboard 📊 - {{ parent() }}{% endblock %} + +{% block content %} +
+
+
+

🎉✨ WELCOME TO THE SUPER AMAZING DRINKS INVENTORY SYSTEM!!! 🍹🥂

+
+
+ +
+ +
+ + {% if showLowStockAlerts %} + {% include 'dashboard/low-stock-alerts.twig' %} + {% endif %} + + +
+
+
🌟 DASHBOARD SUMMARY 🌟
+
+
+

🚀 This AWESOME system helps you manage your drinks inventory 🍺, track stock levels 📊, and create orders 📝!!! 🌟

+ + +
+
+ + +
+
+
🚀 GETTING STARTED 🚀
+
+
+
    +
  1. +
    +
    🍹 Add drink types
    + Define your products and set desired stock levels 🎯 +
    + Add Now +
  2. +
  3. +
    +
    📦 Update inventory
    + Keep your stock levels accurate and up-to-date 🔄 +
    + Update +
  4. +
  5. +
    +
    🛒 Create orders
    + Order more drinks based on inventory needs 📝 +
    + Order +
  6. +
  7. +
    +
    📋 Track orders
    + Update inventory when orders are fulfilled ✅ +
    + View +
  8. +
+
+
+
+ + +
+ + {% if showQuickUpdateForm %} + {% include 'dashboard/quick-update-form.twig' %} + {% endif %} + + +
+
+
💫 FUN FACTS ABOUT DRINKS!!! 💫
+
+
+
+
+ 🍺 Beer is one of the oldest and most widely consumed alcoholic drinks in the world! 🌍 +
+
+ 🍷 Wine has been produced for thousands of years! The earliest evidence of wine is from Georgia (6000 BC)! 🕰️ +
+
+ 🍹 The world's most expensive cocktail is the "Diamonds Are Forever" martini, costing $22,600! 💎 +
+
+ 🥤 The average person will drink about 20,000 beverages in their lifetime! 😮 +
+
+
+
+ + +
+
+
💪 MOTIVATION FOR TODAY!!! 💪
+
+
+
+ 🌟 Keep your inventory STOCKED and your customers HAPPY! 😄 You're doing AMAZING work! 🎉 +
+
+ 🚀 Remember: Every bottle tracked is a problem solved! 🧠 Stay AWESOME and keep CRUSHING it! 💯 +
+
+ +
+
+
+
+{% endblock %} diff --git a/templates/dashboard/low-stock-alerts.twig b/templates/dashboard/low-stock-alerts.twig new file mode 100644 index 0000000..8add9de --- /dev/null +++ b/templates/dashboard/low-stock-alerts.twig @@ -0,0 +1,49 @@ +{# 🚨 Super Important Low Stock Alerts Component 🚨 #} + +
+
+
🚨 OMG!!! LOW STOCK ALERTS!!! 🚨
+
+
+ {% if lowStockItems is empty %} +

✅✅✅ WOOHOO!!! All stock levels are SUPER ADEQUATE!!! 🎉🎊 PARTY TIME!!! 🥳🍾

+ {% else %} +
+
😱 OH NO!!! WE'RE RUNNING OUT OF DRINKS!!! 😱
+

🆘 EMERGENCY SITUATION!!! 🆘 We need to restock ASAP!!! ⏰⏰⏰

+
+
+ + + + + + + + + + + + {% for item in lowStockItems %} + + + + + + + + {% endfor %} + +
🍹 Drink Type 🍸📉 Current Stock 📊🎯 Desired Stock 🏆⚠️ Difference ⚠️🛠️ Actions 🛠️
🥤 {{ item.drinkType.name }} 🥤📉 {{ item.currentStock }} 📉🎯 {{ item.desiredStock }} 🎯⚠️ {{ item.currentStock - item.desiredStock }} ⚠️ + 🔄 Update Stock NOW!!! 🔄 +
+
+ +
+

⏰ Don't delay! Your customers are THIRSTY!!! 🥵 Act NOW before it's TOO LATE!!! ⏰

+
+ {% endif %} +
+
diff --git a/templates/dashboard/quick-update-form.twig b/templates/dashboard/quick-update-form.twig new file mode 100644 index 0000000..8d45219 --- /dev/null +++ b/templates/dashboard/quick-update-form.twig @@ -0,0 +1,48 @@ +{# ⚡⚡⚡ SUPER FAST Quick Update Form Component ⚡⚡⚡ #} +{% import 'components/form.twig' as form %} + +
+
+
⚡ LIGHTNING FAST STOCK UPDATE!!! ⚡
+
+
+
+ {{ form.errors(error) }} + +
+
🏎️ ZOOM ZOOM!!! 🏎️
+

⏱️ Update your stock in SECONDS!!! ⏱️ SO FAST!!! SO EASY!!! 🤩

+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ +
+
+
+ + +
diff --git a/templates/drink-types/create.twig b/templates/drink-types/create.twig new file mode 100644 index 0000000..b932101 --- /dev/null +++ b/templates/drink-types/create.twig @@ -0,0 +1,36 @@ +{% extends 'layout.twig' %} +{% import 'components/form.twig' as form %} + +{% block title %}Add New Drink Type - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

Add New Drink Type

+
+ +
+ +
+
+ {{ form.errors(error) }} + +
+ {{ form.input('name', 'Name', data.name|default(''), 'text', true) }} + + {{ form.textarea('description', 'Description', data.description|default(''), false, 3, 'Enter a description of the drink type') }} + + {{ form.input('desired_stock', 'Desired Stock', data.desired_stock|default('10'), 'number', true, '', 'form-control-sm') }} + +
+ {{ form.submit('Create Drink Type') }} + Cancel +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/drink-types/edit.twig b/templates/drink-types/edit.twig new file mode 100644 index 0000000..922e68c --- /dev/null +++ b/templates/drink-types/edit.twig @@ -0,0 +1,36 @@ +{% extends 'layout.twig' %} +{% import 'components/form.twig' as form %} + +{% block title %}Edit Drink Type - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

Edit Drink Type

+
+ +
+ +
+
+ {{ form.errors(error) }} + +
+ {{ form.input('name', 'Name', drinkType.name, 'text', true) }} + + {{ form.textarea('description', 'Description', drinkType.description, false, 3, 'Enter a description of the drink type') }} + + {{ form.input('desired_stock', 'Desired Stock', drinkType.desiredStock, 'number', true, '', 'form-control-sm') }} + +
+ {{ form.submit('Update Drink Type') }} + Cancel +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/drink-types/index.twig b/templates/drink-types/index.twig new file mode 100644 index 0000000..ab888da --- /dev/null +++ b/templates/drink-types/index.twig @@ -0,0 +1,73 @@ +{% extends 'layout.twig' %} + +{% block title %}Drink Types - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

Drink Types

+
+ +
+ + {% if drinkTypes is empty %} +
+ No drink types found. Click the button above to add your first drink type. +
+ {% else %} +
+ + + + + + + + + + + + + {% for drinkType in drinkTypes %} + + + + + + + + + {% endfor %} + +
IDNameDescriptionDesired StockCurrent StockActions
{{ drinkType.id }}{{ drinkType.name }}{{ drinkType.description|default('-') }}{{ drinkType.desiredStock }} + {% if drinkType.currentStock is defined %} + {% if drinkType.currentStock < drinkType.desiredStock %} + {{ drinkType.currentStock }} + {% else %} + {{ drinkType.currentStock }} + {% endif %} + {% else %} + - + {% endif %} + +
+ + View + + + Edit + +
+ +
+
+
+
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/drink-types/show.twig b/templates/drink-types/show.twig new file mode 100644 index 0000000..810690a --- /dev/null +++ b/templates/drink-types/show.twig @@ -0,0 +1,141 @@ +{% extends 'layout.twig' %} + +{% block title %}{{ drinkType.name }} - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

Drink Type Details

+
+
+
+ + Back to List + + + Edit + +
+ +
+
+
+
+ +
+
+
+
+
Basic Information
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{ drinkType.id }}
Name{{ drinkType.name }}
Description{{ drinkType.description|default('No description provided') }}
Desired Stock{{ drinkType.desiredStock }}
Current Stock + {% if currentStock is defined %} + {% if currentStock < drinkType.desiredStock %} + {{ currentStock }} + {% else %} + {{ currentStock }} + {% endif %} + {% else %} + Not available + {% endif %} +
Created At{{ drinkType.createdAt|date('Y-m-d H:i:s') }}
Updated At{{ drinkType.updatedAt|date('Y-m-d H:i:s') }}
+
+
+
+ +
+
+
+
Stock History
+
+
+ {% if inventoryRecords is defined and inventoryRecords is not empty %} +
+ + + + + + + + + + {% for record in inventoryRecords %} + + + + + + {% endfor %} + +
DateQuantityChange
{{ record.createdAt|date('Y-m-d H:i:s') }}{{ record.quantity }} + {% if record.change > 0 %} + +{{ record.change }} + {% elseif record.change < 0 %} + {{ record.change }} + {% else %} + 0 + {% endif %} +
+
+ + {% else %} +

No inventory records found for this drink type.

+ {% endif %} +
+
+ +
+
+
Quick Actions
+
+ +
+
+
+{% endblock %} diff --git a/templates/error.twig b/templates/error.twig new file mode 100644 index 0000000..ca09af6 --- /dev/null +++ b/templates/error.twig @@ -0,0 +1,25 @@ +{% extends "layout.twig" %} + +{% block title %}Error {{ status_code }} - {{ parent() }}{% endblock %} + +{% block content %} +
+

Error {{ status_code }}

+ +
+

{{ error.message }}

+
+ + {% if error.trace is defined %} +
+

Error Details

+

File: {{ error.file }} (line {{ error.line }})

+
{{ error.trace }}
+
+ {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/templates/inventory/history.twig b/templates/inventory/history.twig new file mode 100644 index 0000000..122aeb8 --- /dev/null +++ b/templates/inventory/history.twig @@ -0,0 +1,172 @@ +{% extends 'layout.twig' %} + +{% block title %}Inventory History - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

+ {% if drinkType is defined %} + Inventory History for {{ drinkType.name }} + {% else %} + Inventory History + {% endif %} +

+
+
+
+ + Back to Inventory + + {% if drinkType is defined %} + + All History + + {% endif %} + + Update Stock + +
+
+
+ + {% if inventoryRecords is empty %} +
+ No inventory records found. +
+ {% else %} +
+
+
+
+
Stock Change History
+
+
+ +
+
+
+
+
+ + + + + {% if drinkType is not defined %} + + {% endif %} + + + + + + + {% for record in inventoryRecords %} + + + {% if drinkType is not defined %} + + {% endif %} + + + + + {% endfor %} + +
DateDrink TypePrevious QuantityNew QuantityChange
{{ record.createdAt|date('Y-m-d H:i:s') }} + + {{ record.drinkType.name }} + + {{ record.previousQuantity|default('-') }}{{ record.quantity }} + {% if record.change is defined %} + {% if record.change > 0 %} + +{{ record.change }} + {% elseif record.change < 0 %} + {{ record.change }} + {% else %} + 0 + {% endif %} + {% else %} + Initial + {% endif %} +
+
+
+ +
+ + {% if drinkType is defined %} +
+
+
+
+
Stock Level Trend
+
+
+

Stock level trend visualization would be displayed here.

+
+ This is a placeholder for a chart showing the stock level trend over time. +
+
+
+
+
+
+
+
Drink Type Information
+
+
+ + + + + + + + + + + + + + + + + +
Name{{ drinkType.name }}
Description{{ drinkType.description|default('No description provided') }}
Desired Stock{{ drinkType.desiredStock }}
Current Stock + {% if drinkType.currentStock is defined %} + {% if drinkType.currentStock < drinkType.desiredStock %} + {{ drinkType.currentStock }} + {% else %} + {{ drinkType.currentStock }} + {% endif %} + {% else %} + Not available + {% endif %} +
+ +
+
+
+
+ {% endif %} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/inventory/index.twig b/templates/inventory/index.twig new file mode 100644 index 0000000..033b4b1 --- /dev/null +++ b/templates/inventory/index.twig @@ -0,0 +1,175 @@ +{% extends 'layout.twig' %} + +{% block title %}Inventory Overview - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

Inventory Overview

+
+ +
+ + {% if stockLevels is empty %} +
+ No drink types found in inventory. Add a drink type to get started. +
+ {% else %} +
+
+
+
+
Current Stock Levels
+
+
+
+ + + + + + + + + + + + + {% for item in stockLevels %} + + + + + + + + + {% endfor %} + +
Drink TypeCurrent StockDesired StockDifferenceStatusActions
+ + {{ item.drinkType.name }} + + {{ item.currentStock }}{{ item.drinkType.desiredStock }} + {% set difference = item.currentStock - item.drinkType.desiredStock %} + {% if difference < 0 %} + {{ difference }} + {% elseif difference > 0 %} + +{{ difference }} + {% else %} + 0 + {% endif %} + + {% if item.currentStock < item.drinkType.desiredStock %} + Low Stock + {% elseif item.currentStock == item.drinkType.desiredStock %} + Optimal + {% else %} + Excess + {% endif %} + + +
+
+
+
+
+ +
+
+
+
Inventory Summary
+
+
+ {% set totalItems = stockLevels|length %} + {% set lowStockCount = 0 %} + {% set optimalCount = 0 %} + {% set excessCount = 0 %} + + {% for item in stockLevels %} + {% if item.currentStock < item.drinkType.desiredStock %} + {% set lowStockCount = lowStockCount + 1 %} + {% elseif item.currentStock == item.drinkType.desiredStock %} + {% set optimalCount = optimalCount + 1 %} + {% else %} + {% set excessCount = excessCount + 1 %} + {% endif %} + {% endfor %} + +
+
Total Drink Types: {{ totalItems }}
+
+ +
+
+ Low Stock: + {{ lowStockCount }} +
+
+
+
+
+ +
+
+ Optimal Stock: + {{ optimalCount }} +
+
+
+
+
+ +
+
+ Excess Stock: + {{ excessCount }} +
+
+
+
+
+
+
+ + +
+
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/inventory/low-stock.twig b/templates/inventory/low-stock.twig new file mode 100644 index 0000000..81e3711 --- /dev/null +++ b/templates/inventory/low-stock.twig @@ -0,0 +1,159 @@ +{% extends 'layout.twig' %} + +{% block title %}Low Stock Items - {{ parent() }}{% endblock %} + +{% block content %} + + + {% if lowStockItems is empty %} +
+ All items are at or above their desired stock levels. +
+ {% else %} +
+
+
Items Below Desired Stock Levels
+
+
+
+ + + + + + + + + + + + + {% for item in lowStockItems %} + + + + + + + + + {% endfor %} + +
Drink TypeCurrent StockDesired StockDifferenceStatusActions
+ + {{ item.drinkType.name }} + + {{ item.currentStock }}{{ item.drinkType.desiredStock }}{{ item.currentStock - item.drinkType.desiredStock }} + {% set percentage = (item.currentStock / item.drinkType.desiredStock * 100)|round %} + {% if percentage < 25 %} + Critical ({{ percentage }}%) + {% elseif percentage < 50 %} + Low ({{ percentage }}%) + {% else %} + Below Target ({{ percentage }}%) + {% endif %} + + +
+
+
+ +
+ +
+
+
+
+
Recommended Actions
+
+
+
    +
  • + Create an order to replenish stock +
  • +
  • + Review consumption patterns +
  • +
  • + Adjust desired stock levels if needed +
  • +
+
+
+
+
+
+
+
Stock Level Summary
+
+
+
+ {{ lowStockItems|length }} items are below their desired stock levels. +
+ + {% set criticalCount = 0 %} + {% set lowCount = 0 %} + {% set belowTargetCount = 0 %} + + {% for item in lowStockItems %} + {% set percentage = (item.currentStock / item.drinkType.desiredStock * 100)|round %} + {% if percentage < 25 %} + {% set criticalCount = criticalCount + 1 %} + {% elseif percentage < 50 %} + {% set lowCount = lowCount + 1 %} + {% else %} + {% set belowTargetCount = belowTargetCount + 1 %} + {% endif %} + {% endfor %} + +
+
+ Critical (< 25%): + {{ criticalCount }} +
+
+ Low (25-50%): + {{ lowCount }} +
+
+ Below Target (> 50%): + {{ belowTargetCount }} +
+
+
+
+
+
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/inventory/update.twig b/templates/inventory/update.twig new file mode 100644 index 0000000..8af53f6 --- /dev/null +++ b/templates/inventory/update.twig @@ -0,0 +1,88 @@ +{% extends 'layout.twig' %} +{% import 'components/form.twig' as form %} + +{% block title %}Update Stock - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

Update Stock Levels

+
+ +
+ +
+
+
+
+
Update Single Item
+
+
+ {{ form.errors(error) }} + +
+
+ + +
+ +
+ + + Enter the new total quantity, not the amount to add/remove. +
+ +
+ {{ form.submit('Update Stock') }} +
+
+
+
+
+ +
+
+
+
Stock Update Tips
+
+
+
+
How to update stock:
+
    +
  1. Select the drink type you want to update
  2. +
  3. Enter the total current quantity (not the difference)
  4. +
  5. Click "Update Stock" to save the changes
  6. +
+
+ +
+
Important Notes:
+
    +
  • Stock updates are logged in the inventory history
  • +
  • The system will calculate the difference from the previous stock level
  • +
  • If stock falls below desired levels, it will appear in low stock alerts
  • +
+
+ + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/layout.twig b/templates/layout.twig new file mode 100644 index 0000000..d45eb52 --- /dev/null +++ b/templates/layout.twig @@ -0,0 +1,70 @@ + + + + + + 🍻🥂 {% block title %}Saufen{% endblock %} 🍹🥤 + + + + + + + + +
+ +
+ +
+ {% include 'components/flash-messages.twig' %} + + {% block content %}{% endblock %} +
+ +
+
+
+
+

🌟✨ © {{ "now"|date("Y") }} 🎉 {{ appName }} 🎊

+
+
+

Made with 💖 and 🍺 ✨🌟

+
+
+
+
+ + + + + + {% block scripts %}{% endblock %} + + diff --git a/templates/orders/create.twig b/templates/orders/create.twig new file mode 100644 index 0000000..91dcb8d --- /dev/null +++ b/templates/orders/create.twig @@ -0,0 +1,204 @@ +{% extends 'layout.twig' %} +{% import 'components/form.twig' as form %} + +{% block title %}Create Order - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

Create New Order

+
+ +
+ +
+
+ {{ form.errors(error) }} + +
+
+
+
+
Order Items
+
+
+ +
+
+ +
+
+
+
+
+
+ + +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ + +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+ +
+ Please add at least one item to the order. +
+
+ +
+ {{ form.submit('Create Order') }} + Cancel +
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/orders/index.twig b/templates/orders/index.twig new file mode 100644 index 0000000..1f6374d --- /dev/null +++ b/templates/orders/index.twig @@ -0,0 +1,179 @@ +{% extends 'layout.twig' %} + +{% block title %}Orders - {{ parent() }}{% endblock %} + +{% block content %} + + + {% if orders is empty %} +
+ No orders found. Click one of the buttons above to create your first order. +
+ {% else %} +
+
+
+
+
Order List
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + {% endfor %} + +
IDDateItemsTotal QuantityStatusActions
{{ order.id }}{{ order.createdAt|date('Y-m-d H:i:s') }}{{ order.orderItems|length }} + {% set totalQuantity = 0 %} + {% for item in order.orderItems %} + {% set totalQuantity = totalQuantity + item.quantity %} + {% endfor %} + {{ totalQuantity }} + + {% if order.status == 'pending' %} + Pending + {% elseif order.status == 'completed' %} + Completed + {% elseif order.status == 'cancelled' %} + Cancelled + {% else %} + {{ order.status }} + {% endif %} + +
+ + + + {% if order.status == 'pending' %} +
+ + +
+
+ + +
+ {% endif %} +
+
+
+
+ +
+ +
+
+
+
+
Order Statistics
+
+
+ {% set pendingCount = 0 %} + {% set completedCount = 0 %} + {% set cancelledCount = 0 %} + + {% for order in orders %} + {% if order.status == 'pending' %} + {% set pendingCount = pendingCount + 1 %} + {% elseif order.status == 'completed' %} + {% set completedCount = completedCount + 1 %} + {% elseif order.status == 'cancelled' %} + {% set cancelledCount = cancelledCount + 1 %} + {% endif %} + {% endfor %} + +
+
+ Pending Orders: + {{ pendingCount }} +
+
+ Completed Orders: + {{ completedCount }} +
+
+ Cancelled Orders: + {{ cancelledCount }} +
+
+
+
+
+ +
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/orders/show.twig b/templates/orders/show.twig new file mode 100644 index 0000000..68f1b59 --- /dev/null +++ b/templates/orders/show.twig @@ -0,0 +1,252 @@ +{% extends 'layout.twig' %} +{% import 'components/form.twig' as form %} + +{% block title %}Order #{{ order.id }} - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

Order #{{ order.id }}

+
+
+
+ + Back to Orders + + {% if order.status == 'pending' %} + + + {% endif %} +
+
+
+ +
+
+
+
+
Order Items
+
+
+ {% if order.orderItems is empty %} +
+ This order has no items. +
+ {% else %} +
+ + + + + + {% if order.status == 'pending' %} + + {% endif %} + + + + {% for item in order.orderItems %} + + + + {% if order.status == 'pending' %} + + {% endif %} + + {% endfor %} + + + + + + {% if order.status == 'pending' %} + + {% endif %} + + +
Drink TypeQuantityActions
+ + {{ item.drinkType.name }} + + {{ item.quantity }} +
+ +
+
Total + {% set totalQuantity = 0 %} + {% for item in order.orderItems %} + {% set totalQuantity = totalQuantity + item.quantity %} + {% endfor %} + {{ totalQuantity }} +
+
+ {% endif %} + + {% if order.status == 'pending' %} +
+ +
+ {% endif %} +
+
+
+ +
+
+
+
Order Details
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Order ID{{ order.id }}
Status + {% if order.status == 'pending' %} + Pending + {% elseif order.status == 'completed' %} + Completed + {% elseif order.status == 'cancelled' %} + Cancelled + {% else %} + {{ order.status }} + {% endif %} +
Created At{{ order.createdAt|date('Y-m-d H:i:s') }}
Updated At{{ order.updatedAt|date('Y-m-d H:i:s') }}
Total Items{{ order.orderItems|length }}
Total Quantity + {% set totalQuantity = 0 %} + {% for item in order.orderItems %} + {% set totalQuantity = totalQuantity + item.quantity %} + {% endfor %} + {{ totalQuantity }} +
+
+
+ + {% if order.status == 'pending' %} +
+
+
Danger Zone
+
+
+
+
+ +
+
+
+
+ {% endif %} +
+
+ + {% if order.status == 'pending' %} + + + + + + + + + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/settings/index.twig b/templates/settings/index.twig new file mode 100644 index 0000000..1077222 --- /dev/null +++ b/templates/settings/index.twig @@ -0,0 +1,169 @@ +{% extends 'layout.twig' %} +{% import 'components/form.twig' as form %} + +{% block title %}⚙️🔧 System Settings 🛠️🔩 - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

⚙️✨ SUPER AWESOME SYSTEM SETTINGS 🔧🌟

+
+
+ +
+
+ +
+
+ {{ form.errors(error) }} + +
+
+
+
📊 Stock Adjustment Settings 📈🔄
+ +
+ + + 🧮 Number of recent orders to analyze for consumption patterns (default: 5) 📝 +
+ +
+ + + 🔼 Maximum percentage to adjust stock levels by (default: 20%) 🔽 +
+ +
+ + + ⚠️ Minimum difference required to trigger an adjustment (default: 10%) 🚦 +
+
+ +
+
🏪 Inventory Settings 📦🗃️
+ +
+ + + ⚠️ Percentage of desired stock below which items are considered low stock (default: 50%) 😱 +
+ +
+ + + 🆘 Percentage of desired stock below which items are considered critical (default: 25%) 😭 +
+ +
+ + + 🌟 Default desired stock level for new drink types (default: 10) 🍹 +
+
+
+ +
+
+
🖥️ System Settings 💻🌐
+ +
+ + + 👑 Name of the system displayed in the header 📛 +
+ +
+
+ + +
+ 🧠 Automatically adjust desired stock levels based on consumption patterns 📊 +
+
+ +
+
🎨 Display Settings 📱💅
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+ + + 🔢 Number of items to display per page in lists 📊 +
+
+
+ +
+ {{ form.submit('💾 SAVE AWESOME SETTINGS 🚀') }} +
+
+
+
+ + + +{% endblock %} diff --git a/tests/Feature/DatabaseTest.php b/tests/Feature/DatabaseTest.php new file mode 100644 index 0000000..ea7a954 --- /dev/null +++ b/tests/Feature/DatabaseTest.php @@ -0,0 +1,26 @@ +setUpDB(); + } + public function testDatabase(): void + { + /** @var DrinkTypeRepository $drinkRepository */ + $drinkRepository = $this->container->get(DrinkTypeRepository::class); + + $this->assertCount(0, $drinkRepository->findAll()); + } +} diff --git a/tests/Feature/Repository/DrinkTypeRepositoryTest.php b/tests/Feature/Repository/DrinkTypeRepositoryTest.php new file mode 100644 index 0000000..b3fa9e0 --- /dev/null +++ b/tests/Feature/Repository/DrinkTypeRepositoryTest.php @@ -0,0 +1,122 @@ +setUpDB(); + $this->repository = $this->container->get(DrinkTypeRepository::class); + } + + public function testFindAll(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->repository->findAll()); + + // Create some drink types + $drinkType1 = new DrinkType('Cola', 'Refreshing cola drink', 10); + $drinkType2 = new DrinkType('Fanta', 'Orange soda', 5); + + // Save them to the repository + $this->repository->save($drinkType1); + $this->repository->save($drinkType2); + + // Now findAll should return both drink types + $drinkTypes = $this->repository->findAll(); + $this->assertCount(2, $drinkTypes); + $this->assertContainsOnlyInstancesOf(DrinkType::class, $drinkTypes); + } + + public function testFind(): void + { + // Create a drink type + $drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + $this->repository->save($drinkType); + + // Get the ID + $id = $drinkType->getId(); + $this->assertNotNull($id); + + // Find by ID + $foundDrinkType = $this->repository->find($id); + $this->assertInstanceOf(DrinkType::class, $foundDrinkType); + $this->assertEquals('Cola', $foundDrinkType->getName()); + $this->assertEquals('Refreshing cola drink', $foundDrinkType->getDescription()); + $this->assertEquals(10, $foundDrinkType->getDesiredStock()); + + // Try to find a non-existent ID + $nonExistentId = 9999; + $this->assertNull($this->repository->find($nonExistentId)); + } + + public function testFindOneBy(): void + { + // Create a drink type + $drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + $this->repository->save($drinkType); + + // Find by name + $foundDrinkType = $this->repository->findOneBy(['name' => 'Cola']); + $this->assertInstanceOf(DrinkType::class, $foundDrinkType); + $this->assertEquals('Cola', $foundDrinkType->getName()); + + // Try to find a non-existent name + $this->assertNull($this->repository->findOneBy(['name' => 'NonExistent'])); + } + + public function testSave(): void + { + // Create a drink type + $drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + + // Save it + $this->repository->save($drinkType); + + // Check that it was saved + $id = $drinkType->getId(); + $this->assertNotNull($id); + + // Find it by ID to confirm it's in the database + $foundDrinkType = $this->repository->find($id); + $this->assertInstanceOf(DrinkType::class, $foundDrinkType); + $this->assertEquals('Cola', $foundDrinkType->getName()); + + // Update it + $drinkType->setName('Pepsi'); + $this->repository->save($drinkType); + + // Find it again to confirm the update + $foundDrinkType = $this->repository->find($id); + $this->assertEquals('Pepsi', $foundDrinkType->getName()); + } + + public function testRemove(): void + { + // Create a drink type + $drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + $this->repository->save($drinkType); + + // Get the ID + $id = $drinkType->getId(); + $this->assertNotNull($id); + + // Remove it + $this->repository->remove($drinkType); + + // Try to find it by ID to confirm it's gone + $this->assertNull($this->repository->find($id)); + } +} diff --git a/tests/Feature/Repository/InventoryRecordRepositoryTest.php b/tests/Feature/Repository/InventoryRecordRepositoryTest.php new file mode 100644 index 0000000..28e521d --- /dev/null +++ b/tests/Feature/Repository/InventoryRecordRepositoryTest.php @@ -0,0 +1,197 @@ +setUpDB(); + $this->repository = $this->container->get(InventoryRecordRepository::class); + $this->drinkTypeRepository = $this->container->get(DrinkTypeRepository::class); + + // Create a drink type for testing + $this->drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + $this->drinkTypeRepository->save($this->drinkType); + } + + public function testFindAll(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->repository->findAll()); + + // Create some inventory records + $record1 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-02')); + + // Save them to the repository + $this->repository->save($record1); + $this->repository->save($record2); + + // Now findAll should return both records + $records = $this->repository->findAll(); + $this->assertCount(2, $records); + $this->assertContainsOnlyInstancesOf(InventoryRecord::class, $records); + } + + public function testFind(): void + { + // Create an inventory record + $record = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $this->repository->save($record); + + // Get the ID + $id = $record->getId(); + $this->assertNotNull($id); + + // Find by ID + $foundRecord = $this->repository->find($id); + $this->assertInstanceOf(InventoryRecord::class, $foundRecord); + $this->assertEquals(5, $foundRecord->getQuantity()); + $this->assertEquals('2023-01-01', $foundRecord->getTimestamp()->format('Y-m-d')); + + // Try to find a non-existent ID + $nonExistentId = 9999; + $this->assertNull($this->repository->find($nonExistentId)); + } + + public function testFindByDrinkType(): void + { + // Create another drink type + $anotherDrinkType = new DrinkType('Fanta', 'Orange soda', 5); + $this->drinkTypeRepository->save($anotherDrinkType); + + // Create inventory records for both drink types + $record1 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-02')); + $record3 = new InventoryRecord($anotherDrinkType, 15, new DateTimeImmutable('2023-01-03')); + + // Save them to the repository + $this->repository->save($record1); + $this->repository->save($record2); + $this->repository->save($record3); + + // Find by drink type + $records = $this->repository->findByDrinkType($this->drinkType); + $this->assertCount(2, $records); + $this->assertContainsOnlyInstancesOf(InventoryRecord::class, $records); + + // Check that the records are for the correct drink type + foreach ($records as $record) { + $this->assertEquals($this->drinkType->getId(), $record->getDrinkType()->getId()); + } + + // Find by the other drink type + $otherRecords = $this->repository->findByDrinkType($anotherDrinkType); + $this->assertCount(1, $otherRecords); + $this->assertEquals($anotherDrinkType->getId(), $otherRecords[0]->getDrinkType()->getId()); + } + + public function testFindLatestByDrinkType(): void + { + // Create inventory records with different timestamps + $record1 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-02')); + $record3 = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-03')); + + // Save them to the repository + $this->repository->save($record1); + $this->repository->save($record2); + $this->repository->save($record3); + + // Find the latest record + $latestRecord = $this->repository->findLatestByDrinkType($this->drinkType); + $this->assertInstanceOf(InventoryRecord::class, $latestRecord); + $this->assertEquals(15, $latestRecord->getQuantity()); + $this->assertEquals('2023-01-03', $latestRecord->getTimestamp()->format('Y-m-d')); + } + + public function testFindByTimestampRange(): void + { + // Create inventory records with different timestamps + $record1 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-02')); + $record3 = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-03')); + $record4 = new InventoryRecord($this->drinkType, 20, new DateTimeImmutable('2023-01-04')); + $record5 = new InventoryRecord($this->drinkType, 25, new DateTimeImmutable('2023-01-05')); + + // Save them to the repository + $this->repository->save($record1); + $this->repository->save($record2); + $this->repository->save($record3); + $this->repository->save($record4); + $this->repository->save($record5); + + // Find records in a specific range + $start = new DateTimeImmutable('2023-01-02'); + $end = new DateTimeImmutable('2023-01-04'); + $records = $this->repository->findByTimestampRange($start, $end); + + $this->assertCount(3, $records); + + // Check that the records are within the range + foreach ($records as $record) { + $timestamp = $record->getTimestamp(); + $this->assertTrue($timestamp >= $start && $timestamp <= $end); + } + } + + public function testSave(): void + { + // Create an inventory record + $record = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + + // Save it + $this->repository->save($record); + + // Check that it was saved + $id = $record->getId(); + $this->assertNotNull($id); + + // Find it by ID to confirm it's in the database + $foundRecord = $this->repository->find($id); + $this->assertInstanceOf(InventoryRecord::class, $foundRecord); + $this->assertEquals(5, $foundRecord->getQuantity()); + + // Update it + $record->setQuantity(10); + $this->repository->save($record); + + // Find it again to confirm the update + $foundRecord = $this->repository->find($id); + $this->assertEquals(10, $foundRecord->getQuantity()); + } + + public function testRemove(): void + { + // Create an inventory record + $record = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $this->repository->save($record); + + // Get the ID + $id = $record->getId(); + $this->assertNotNull($id); + + // Remove it + $this->repository->remove($record); + + // Try to find it by ID to confirm it's gone + $this->assertNull($this->repository->find($id)); + } +} diff --git a/tests/Feature/Repository/OrderItemRepositoryTest.php b/tests/Feature/Repository/OrderItemRepositoryTest.php new file mode 100644 index 0000000..cd4d067 --- /dev/null +++ b/tests/Feature/Repository/OrderItemRepositoryTest.php @@ -0,0 +1,226 @@ +setUpDB(); + $this->repository = $this->container->get(OrderItemRepository::class); + $this->drinkTypeRepository = $this->container->get(DrinkTypeRepository::class); + $this->orderRepository = $this->container->get(OrderRepository::class); + + // Create a drink type for testing + $this->drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + $this->drinkTypeRepository->save($this->drinkType); + + // Create an order for testing + $this->order = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + $this->orderRepository->save($this->order); + } + + public function testFindAll(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->repository->findAll()); + + // Create some order items + $orderItem1 = new OrderItem($this->drinkType, 5, $this->order); + $orderItem2 = new OrderItem($this->drinkType, 10, $this->order); + + // Save them to the repository + $this->repository->save($orderItem1); + $this->repository->save($orderItem2); + + // Now findAll should return both order items + $orderItems = $this->repository->findAll(); + $this->assertCount(2, $orderItems); + $this->assertContainsOnlyInstancesOf(OrderItem::class, $orderItems); + } + + public function testFind(): void + { + // Create an order item + $orderItem = new OrderItem($this->drinkType, 5, $this->order); + $this->repository->save($orderItem); + + // Get the ID + $id = $orderItem->getId(); + $this->assertNotNull($id); + + // Find by ID + $foundOrderItem = $this->repository->find($id); + $this->assertInstanceOf(OrderItem::class, $foundOrderItem); + $this->assertEquals(5, $foundOrderItem->getQuantity()); + $this->assertEquals($this->drinkType->getId(), $foundOrderItem->getDrinkType()->getId()); + $this->assertEquals($this->order->getId(), $foundOrderItem->getOrder()->getId()); + + // Try to find a non-existent ID + $nonExistentId = 9999; + $this->assertNull($this->repository->find($nonExistentId)); + } + + public function testFindByOrder(): void + { + // Create another order + $anotherOrder = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-02')); + $this->orderRepository->save($anotherOrder); + + // Create order items for both orders + $orderItem1 = new OrderItem($this->drinkType, 5, $this->order); + $orderItem2 = new OrderItem($this->drinkType, 10, $this->order); + $orderItem3 = new OrderItem($this->drinkType, 15, $anotherOrder); + + // Save them to the repository + $this->repository->save($orderItem1); + $this->repository->save($orderItem2); + $this->repository->save($orderItem3); + + // Find by order + $orderItems = $this->repository->findByOrder($this->order); + $this->assertCount(2, $orderItems); + $this->assertContainsOnlyInstancesOf(OrderItem::class, $orderItems); + + // Check that the order items are for the correct order + foreach ($orderItems as $orderItem) { + $this->assertEquals($this->order->getId(), $orderItem->getOrder()->getId()); + } + + // Find by the other order + $otherOrderItems = $this->repository->findByOrder($anotherOrder); + $this->assertCount(1, $otherOrderItems); + $this->assertEquals($anotherOrder->getId(), $otherOrderItems[0]->getOrder()->getId()); + } + + public function testFindByDrinkType(): void + { + // Create another drink type + $anotherDrinkType = new DrinkType('Fanta', 'Orange soda', 5); + $this->drinkTypeRepository->save($anotherDrinkType); + + // Create order items for both drink types + $orderItem1 = new OrderItem($this->drinkType, 5, $this->order); + $orderItem2 = new OrderItem($this->drinkType, 10, $this->order); + $orderItem3 = new OrderItem($anotherDrinkType, 15, $this->order); + + // Save them to the repository + $this->repository->save($orderItem1); + $this->repository->save($orderItem2); + $this->repository->save($orderItem3); + + // Find by drink type + $orderItems = $this->repository->findByDrinkType($this->drinkType); + $this->assertCount(2, $orderItems); + $this->assertContainsOnlyInstancesOf(OrderItem::class, $orderItems); + + // Check that the order items are for the correct drink type + foreach ($orderItems as $orderItem) { + $this->assertEquals($this->drinkType->getId(), $orderItem->getDrinkType()->getId()); + } + + // Find by the other drink type + $otherOrderItems = $this->repository->findByDrinkType($anotherDrinkType); + $this->assertCount(1, $otherOrderItems); + $this->assertEquals($anotherDrinkType->getId(), $otherOrderItems[0]->getDrinkType()->getId()); + } + + public function testFindByOrderAndDrinkType(): void + { + // Create another drink type and order + $anotherDrinkType = new DrinkType('Fanta', 'Orange soda', 5); + $this->drinkTypeRepository->save($anotherDrinkType); + + $anotherOrder = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-02')); + $this->orderRepository->save($anotherOrder); + + // Create order items with different combinations + $orderItem1 = new OrderItem($this->drinkType, 5, $this->order); + $orderItem2 = new OrderItem($anotherDrinkType, 10, $this->order); + $orderItem3 = new OrderItem($this->drinkType, 15, $anotherOrder); + $orderItem4 = new OrderItem($anotherDrinkType, 20, $anotherOrder); + + // Save them to the repository + $this->repository->save($orderItem1); + $this->repository->save($orderItem2); + $this->repository->save($orderItem3); + $this->repository->save($orderItem4); + + // Find by order and drink type + $orderItem = $this->repository->findByOrderAndDrinkType($this->order, $this->drinkType); + $this->assertInstanceOf(OrderItem::class, $orderItem); + $this->assertEquals(5, $orderItem->getQuantity()); + $this->assertEquals($this->order->getId(), $orderItem->getOrder()->getId()); + $this->assertEquals($this->drinkType->getId(), $orderItem->getDrinkType()->getId()); + + // Find by another combination + $anotherOrderItem = $this->repository->findByOrderAndDrinkType($anotherOrder, $anotherDrinkType); + $this->assertInstanceOf(OrderItem::class, $anotherOrderItem); + $this->assertEquals(20, $anotherOrderItem->getQuantity()); + $this->assertEquals($anotherOrder->getId(), $anotherOrderItem->getOrder()->getId()); + $this->assertEquals($anotherDrinkType->getId(), $anotherOrderItem->getDrinkType()->getId()); + } + + public function testSave(): void + { + // Create an order item + $orderItem = new OrderItem($this->drinkType, 5, $this->order); + + // Save it + $this->repository->save($orderItem); + + // Check that it was saved + $id = $orderItem->getId(); + $this->assertNotNull($id); + + // Find it by ID to confirm it's in the database + $foundOrderItem = $this->repository->find($id); + $this->assertInstanceOf(OrderItem::class, $foundOrderItem); + $this->assertEquals(5, $foundOrderItem->getQuantity()); + + // Update it + $orderItem->setQuantity(10); + $this->repository->save($orderItem); + + // Find it again to confirm the update + $foundOrderItem = $this->repository->find($id); + $this->assertEquals(10, $foundOrderItem->getQuantity()); + } + + public function testRemove(): void + { + // Create an order item + $orderItem = new OrderItem($this->drinkType, 5, $this->order); + $this->repository->save($orderItem); + + // Get the ID + $id = $orderItem->getId(); + $this->assertNotNull($id); + + // Remove it + $this->repository->remove($orderItem); + + // Try to find it by ID to confirm it's gone + $this->assertNull($this->repository->find($id)); + } +} diff --git a/tests/Feature/Repository/OrderRepositoryTest.php b/tests/Feature/Repository/OrderRepositoryTest.php new file mode 100644 index 0000000..f66bc91 --- /dev/null +++ b/tests/Feature/Repository/OrderRepositoryTest.php @@ -0,0 +1,223 @@ +setUpDB(); + $this->repository = $this->container->get(OrderRepository::class); + $this->drinkTypeRepository = $this->container->get(DrinkTypeRepository::class); + $this->orderItemRepository = $this->container->get(OrderItemRepository::class); + + // Create a drink type for testing + $this->drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + $this->drinkTypeRepository->save($this->drinkType); + } + + public function testFindAll(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->repository->findAll()); + + // Create some orders + $order1 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + $order2 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-02')); + + // Save them to the repository + $this->repository->save($order1); + $this->repository->save($order2); + + // Now findAll should return both orders + $orders = $this->repository->findAll(); + $this->assertCount(2, $orders); + $this->assertContainsOnlyInstancesOf(Order::class, $orders); + } + + public function testFind(): void + { + // Create an order + $order = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + $this->repository->save($order); + + // Get the ID + $id = $order->getId(); + $this->assertNotNull($id); + + // Find by ID + $foundOrder = $this->repository->find($id); + $this->assertInstanceOf(Order::class, $foundOrder); + $this->assertEquals(Order::STATUS_NEW, $foundOrder->getStatus()); + $this->assertEquals('2023-01-01', $foundOrder->getCreatedAt()->format('Y-m-d')); + + // Try to find a non-existent ID + $nonExistentId = 9999; + $this->assertNull($this->repository->find($nonExistentId)); + } + + public function testFindByStatus(): void + { + // Create orders with different statuses + $order1 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + $order2 = new Order(Order::STATUS_IN_WORK, new DateTimeImmutable('2023-01-02')); + $order3 = new Order(Order::STATUS_ORDERED, new DateTimeImmutable('2023-01-03')); + $order4 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-04')); + $order5 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-05')); + + // Save them to the repository + $this->repository->save($order1); + $this->repository->save($order2); + $this->repository->save($order3); + $this->repository->save($order4); + $this->repository->save($order5); + + // Find by status + $newOrders = $this->repository->findByStatus(Order::STATUS_NEW); + $this->assertCount(2, $newOrders); + $this->assertContainsOnlyInstancesOf(Order::class, $newOrders); + + // Check that the orders have the correct status + foreach ($newOrders as $order) { + $this->assertEquals(Order::STATUS_NEW, $order->getStatus()); + } + + // Find by another status + $fulfilledOrders = $this->repository->findByStatus(Order::STATUS_FULFILLED); + $this->assertCount(1, $fulfilledOrders); + $this->assertEquals(Order::STATUS_FULFILLED, $fulfilledOrders[0]->getStatus()); + } + + public function testFindByDateRange(): void + { + // Create orders with different dates + $order1 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + $order2 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-02')); + $order3 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-03')); + $order4 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-04')); + $order5 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-05')); + + // Save them to the repository + $this->repository->save($order1); + $this->repository->save($order2); + $this->repository->save($order3); + $this->repository->save($order4); + $this->repository->save($order5); + + // Find orders in a specific date range + $start = new DateTimeImmutable('2023-01-02'); + $end = new DateTimeImmutable('2023-01-04'); + $orders = $this->repository->findByDateRange($start, $end); + + $this->assertCount(3, $orders); + + // Check that the orders are within the date range + foreach ($orders as $order) { + $createdAt = $order->getCreatedAt(); + $this->assertTrue($createdAt >= $start && $createdAt <= $end); + } + } + + public function testFindLastOrdersForDrinkType(): void + { + // Create orders with different dates + $order1 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-01')); + $order2 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-02')); + $order3 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-03')); + $order4 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-04')); + $order5 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-05')); + + // Save them to the repository + $this->repository->save($order1); + $this->repository->save($order2); + $this->repository->save($order3); + $this->repository->save($order4); + $this->repository->save($order5); + + // Create order items for the drink type + $orderItem1 = new OrderItem($this->drinkType, 5, $order1); + $orderItem2 = new OrderItem($this->drinkType, 10, $order2); + $orderItem3 = new OrderItem($this->drinkType, 15, $order3); + $orderItem4 = new OrderItem($this->drinkType, 20, $order4); + $orderItem5 = new OrderItem($this->drinkType, 25, $order5); + + // Save them to the repository + $this->orderItemRepository->save($orderItem1); + $this->orderItemRepository->save($orderItem2); + $this->orderItemRepository->save($orderItem3); + $this->orderItemRepository->save($orderItem4); + $this->orderItemRepository->save($orderItem5); + + // Find the last 3 orders for the drink type + $orders = $this->repository->findLastOrdersForDrinkType($this->drinkType, 3); + + $this->assertCount(3, $orders); + + // Check that the orders are the most recent ones + $this->assertEquals('2023-01-05', $orders[0]->getCreatedAt()->format('Y-m-d')); + $this->assertEquals('2023-01-04', $orders[1]->getCreatedAt()->format('Y-m-d')); + $this->assertEquals('2023-01-03', $orders[2]->getCreatedAt()->format('Y-m-d')); + } + + public function testSave(): void + { + // Create an order + $order = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + + // Save it + $this->repository->save($order); + + // Check that it was saved + $id = $order->getId(); + $this->assertNotNull($id); + + // Find it by ID to confirm it's in the database + $foundOrder = $this->repository->find($id); + $this->assertInstanceOf(Order::class, $foundOrder); + $this->assertEquals(Order::STATUS_NEW, $foundOrder->getStatus()); + + // Update it + $order->setStatus(Order::STATUS_IN_WORK); + $this->repository->save($order); + + // Find it again to confirm the update + $foundOrder = $this->repository->find($id); + $this->assertEquals(Order::STATUS_IN_WORK, $foundOrder->getStatus()); + } + + public function testRemove(): void + { + // Create an order + $order = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + $this->repository->save($order); + + // Get the ID + $id = $order->getId(); + $this->assertNotNull($id); + + // Remove it + $this->repository->remove($order); + + // Try to find it by ID to confirm it's gone + $this->assertNull($this->repository->find($id)); + } +} diff --git a/tests/Feature/Repository/SystemConfigRepositoryTest.php b/tests/Feature/Repository/SystemConfigRepositoryTest.php new file mode 100644 index 0000000..6b50b36 --- /dev/null +++ b/tests/Feature/Repository/SystemConfigRepositoryTest.php @@ -0,0 +1,167 @@ +setUpDB(); + $this->repository = $this->container->get(SystemConfigRepository::class); + } + + public function testFindAll(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->repository->findAll()); + + // Create some config entries + $config1 = new SystemConfig('key1', 'value1'); + $config2 = new SystemConfig('key2', 'value2'); + + // Save them to the repository + $this->repository->save($config1); + $this->repository->save($config2); + + // Now findAll should return both configs + $configs = $this->repository->findAll(); + $this->assertCount(2, $configs); + $this->assertContainsOnlyInstancesOf(SystemConfig::class, $configs); + } + + public function testFind(): void + { + // Create a config + $config = new SystemConfig('key1', 'value1'); + $this->repository->save($config); + + // Get the ID + $id = $config->getId(); + $this->assertNotNull($id); + + // Find by ID + $foundConfig = $this->repository->find($id); + $this->assertInstanceOf(SystemConfig::class, $foundConfig); + $this->assertEquals('key1', $foundConfig->getKey()); + $this->assertEquals('value1', $foundConfig->getValue()); + + // Try to find a non-existent ID + $nonExistentId = 9999; + $this->assertNull($this->repository->find($nonExistentId)); + } + + public function testFindByKey(): void + { + // Create some configs + $config1 = new SystemConfig('key1', 'value1'); + $config2 = new SystemConfig('key2', 'value2'); + + // Save them to the repository + $this->repository->save($config1); + $this->repository->save($config2); + + // Find by key + $foundConfig = $this->repository->findByKey('key1'); + $this->assertInstanceOf(SystemConfig::class, $foundConfig); + $this->assertEquals('key1', $foundConfig->getKey()); + $this->assertEquals('value1', $foundConfig->getValue()); + + // Try to find a non-existent key + $this->assertNull($this->repository->findByKey('nonexistent')); + } + + public function testGetValue(): void + { + // Create a config + $config = new SystemConfig('key1', 'value1'); + $this->repository->save($config); + + // Get value by key + $value = $this->repository->getValue('key1'); + $this->assertEquals('value1', $value); + + // Get value for a non-existent key (should return default) + $value = $this->repository->getValue('nonexistent', 'default'); + $this->assertEquals('default', $value); + } + + public function testSetValue(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->repository->findAll()); + + // Set a value for a new key + $this->repository->setValue('key1', 'value1'); + + // Check that a new config was created + $configs = $this->repository->findAll(); + $this->assertCount(1, $configs); + $config = $configs[0]; + $this->assertEquals('key1', $config->getKey()); + $this->assertEquals('value1', $config->getValue()); + + // Set a value for an existing key + $this->repository->setValue('key1', 'updated'); + + // Check that the value was updated + $config = $this->repository->findByKey('key1'); + $this->assertInstanceOf(SystemConfig::class, $config); + $this->assertEquals('key1', $config->getKey()); + $this->assertEquals('updated', $config->getValue()); + } + + public function testSave(): void + { + // Create a config + $config = new SystemConfig('key1', 'value1'); + + // Save it + $this->repository->save($config); + + // Check that it was saved + $id = $config->getId(); + $this->assertNotNull($id); + + // Find it by ID to confirm it's in the database + $foundConfig = $this->repository->find($id); + $this->assertInstanceOf(SystemConfig::class, $foundConfig); + $this->assertEquals('key1', $foundConfig->getKey()); + $this->assertEquals('value1', $foundConfig->getValue()); + + // Update it + $config->setValue('updated'); + $this->repository->save($config); + + // Find it again to confirm the update + $foundConfig = $this->repository->find($id); + $this->assertEquals('updated', $foundConfig->getValue()); + } + + public function testRemove(): void + { + // Create a config + $config = new SystemConfig('key1', 'value1'); + $this->repository->save($config); + + // Get the ID + $id = $config->getId(); + $this->assertNotNull($id); + + // Remove it + $this->repository->remove($config); + + // Try to find it by ID to confirm it's gone + $this->assertNull($this->repository->find($id)); + } +} diff --git a/tests/Feature/Service/ConfigurationServiceTest.php b/tests/Feature/Service/ConfigurationServiceTest.php new file mode 100644 index 0000000..9df1158 --- /dev/null +++ b/tests/Feature/Service/ConfigurationServiceTest.php @@ -0,0 +1,201 @@ +setUpDB(); + $this->repository = $this->container->get(SystemConfigRepository::class); + $this->service = $this->container->get(ConfigurationService::class); + } + + public function testGetAllConfigs(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->service->getAllConfigs()); + + // Create some config entries + $config1 = new SystemConfig('key1', 'value1'); + $config2 = new SystemConfig('key2', 'value2'); + $this->repository->save($config1); + $this->repository->save($config2); + + // Now getAllConfigs should return both configs + $configs = $this->service->getAllConfigs(); + $this->assertCount(2, $configs); + $this->assertContainsOnlyInstancesOf(SystemConfig::class, $configs); + } + + public function testGetConfigValue(): void + { + // Create a config + $config = new SystemConfig('key1', 'value1'); + $this->repository->save($config); + + // Get value by key + $value = $this->service->getConfigValue('key1'); + $this->assertEquals('value1', $value); + + // Get value for a non-existent key (should return default) + $value = $this->service->getConfigValue('nonexistent', 'default'); + $this->assertEquals('default', $value); + } + + public function testSetConfigValue(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->service->getAllConfigs()); + + // Set a value for a new key + $this->service->setConfigValue('key1', 'value1'); + + // Check that a new config was created + $configs = $this->service->getAllConfigs(); + $this->assertCount(1, $configs); + $config = $configs[0]; + $this->assertEquals('key1', $config->getKey()); + $this->assertEquals('value1', $config->getValue()); + + // Set a value for an existing key + $this->service->setConfigValue('key1', 'updated'); + + // Check that the value was updated + $config = $this->service->getConfigByKey('key1'); + $this->assertInstanceOf(SystemConfig::class, $config); + $this->assertEquals('key1', $config->getKey()); + $this->assertEquals('updated', $config->getValue()); + } + + public function testGetConfigByKey(): void + { + // Create some configs + $config1 = new SystemConfig('key1', 'value1'); + $config2 = new SystemConfig('key2', 'value2'); + $this->repository->save($config1); + $this->repository->save($config2); + + // Find by key + $foundConfig = $this->service->getConfigByKey('key1'); + $this->assertInstanceOf(SystemConfig::class, $foundConfig); + $this->assertEquals('key1', $foundConfig->getKey()); + $this->assertEquals('value1', $foundConfig->getValue()); + + // Try to find a non-existent key + $this->assertNull($this->service->getConfigByKey('nonexistent')); + } + + public function testCreateConfig(): void + { + // Create a new config + $config = $this->service->createConfig('key1', 'value1'); + + // Check that it was created correctly + $this->assertInstanceOf(SystemConfig::class, $config); + $this->assertEquals('key1', $config->getKey()); + $this->assertEquals('value1', $config->getValue()); + + // Check that it's in the database + $foundConfig = $this->service->getConfigByKey('key1'); + $this->assertInstanceOf(SystemConfig::class, $foundConfig); + $this->assertEquals('key1', $foundConfig->getKey()); + $this->assertEquals('value1', $foundConfig->getValue()); + + // Try to create a config with the same key (should throw an exception) + $this->expectException(InvalidArgumentException::class); + $this->service->createConfig('key1', 'value2'); + } + + public function testUpdateConfig(): void + { + // Create a config + $config = $this->service->createConfig('key1', 'value1'); + + // Update the value + $updatedConfig = $this->service->updateConfig($config, null, 'updated'); + $this->assertEquals('key1', $updatedConfig->getKey()); + $this->assertEquals('updated', $updatedConfig->getValue()); + + // Check that it was updated in the database + $foundConfig = $this->service->getConfigByKey('key1'); + $this->assertEquals('updated', $foundConfig->getValue()); + + // Update the key + $updatedConfig = $this->service->updateConfig($config, 'newKey', null); + $this->assertEquals('newKey', $updatedConfig->getKey()); + $this->assertEquals('updated', $updatedConfig->getValue()); + + // Check that it was updated in the database + $foundConfig = $this->service->getConfigByKey('newKey'); + $this->assertInstanceOf(SystemConfig::class, $foundConfig); + $this->assertEquals('newKey', $foundConfig->getKey()); + $this->assertEquals('updated', $foundConfig->getValue()); + + // Create another config + $config2 = $this->service->createConfig('key2', 'value2'); + + // Try to update the key to an existing key (should throw an exception) + $this->expectException(InvalidArgumentException::class); + $this->service->updateConfig($config2, 'newKey', null); + } + + public function testDeleteConfig(): void + { + // Create a config + $config = $this->service->createConfig('key1', 'value1'); + + // Check that it exists + $this->assertInstanceOf(SystemConfig::class, $this->service->getConfigByKey('key1')); + + // Delete it + $this->service->deleteConfig($config); + + // Check that it's gone + $this->assertNull($this->service->getConfigByKey('key1')); + } + + public function testInitializeDefaultConfigs(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->service->getAllConfigs()); + + // Initialize default configs + $this->service->initializeDefaultConfigs(); + + // Check that the default configs were created + $configs = $this->service->getAllConfigs(); + $this->assertGreaterThanOrEqual(3, count($configs)); // At least 3 default configs + + // Check specific default configs + $lookbackOrders = $this->service->getConfigValue(SystemSettingKey::STOCK_ADJUSTMENT_LOOKBACK_ORDERS->value); + $this->assertEquals('5', $lookbackOrders); + + $magnitude = $this->service->getConfigValue(SystemSettingKey::STOCK_ADJUSTMENT_MAGNITUDE->value); + $this->assertEquals('0.2', $magnitude); + + $threshold = $this->service->getConfigValue(SystemSettingKey::STOCK_ADJUSTMENT_THRESHOLD->value); + $this->assertEquals('0.1', $threshold); + + // Initialize again (should not create duplicates) + $this->service->initializeDefaultConfigs(); + + // Count should be the same + $this->assertCount(count($configs), $this->service->getAllConfigs()); + } +} diff --git a/tests/Feature/Service/DrinkTypeServiceTest.php b/tests/Feature/Service/DrinkTypeServiceTest.php new file mode 100644 index 0000000..f3f20fb --- /dev/null +++ b/tests/Feature/Service/DrinkTypeServiceTest.php @@ -0,0 +1,169 @@ +setUpDB(); + $this->repository = $this->container->get(DrinkTypeRepository::class); + $this->service = $this->container->get(DrinkTypeService::class); + } + + public function testGetAllDrinkTypes(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->service->getAllDrinkTypes()); + + // Create some drink types + $drinkType1 = new DrinkType('Cola', 'Refreshing cola drink', 10); + $drinkType2 = new DrinkType('Fanta', 'Orange soda', 5); + $this->repository->save($drinkType1); + $this->repository->save($drinkType2); + + // Now getAllDrinkTypes should return both drink types + $drinkTypes = $this->service->getAllDrinkTypes(); + $this->assertCount(2, $drinkTypes); + $this->assertContainsOnlyInstancesOf(DrinkType::class, $drinkTypes); + } + + public function testGetDrinkTypeById(): void + { + // Create a drink type + $drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + $this->repository->save($drinkType); + + // Get the ID + $id = $drinkType->getId(); + $this->assertNotNull($id); + + // Find by ID + $foundDrinkType = $this->service->getDrinkTypeById($id); + $this->assertInstanceOf(DrinkType::class, $foundDrinkType); + $this->assertEquals('Cola', $foundDrinkType->getName()); + $this->assertEquals('Refreshing cola drink', $foundDrinkType->getDescription()); + $this->assertEquals(10, $foundDrinkType->getDesiredStock()); + + // Try to find a non-existent ID + $nonExistentId = 9999; + $this->assertNull($this->service->getDrinkTypeById($nonExistentId)); + } + + public function testGetDrinkTypeByName(): void + { + // Create some drink types + $drinkType1 = new DrinkType('Cola', 'Refreshing cola drink', 10); + $drinkType2 = new DrinkType('Fanta', 'Orange soda', 5); + $this->repository->save($drinkType1); + $this->repository->save($drinkType2); + + // Find by name + $foundDrinkType = $this->service->getDrinkTypeByName('Cola'); + $this->assertInstanceOf(DrinkType::class, $foundDrinkType); + $this->assertEquals('Cola', $foundDrinkType->getName()); + $this->assertEquals('Refreshing cola drink', $foundDrinkType->getDescription()); + $this->assertEquals(10, $foundDrinkType->getDesiredStock()); + + // Try to find a non-existent name + $this->assertNull($this->service->getDrinkTypeByName('nonexistent')); + } + + public function testCreateDrinkType(): void + { + // Create a new drink type + $drinkType = $this->service->createDrinkType('Cola', 'Refreshing cola drink', 10); + + // Check that it was created correctly + $this->assertInstanceOf(DrinkType::class, $drinkType); + $this->assertEquals('Cola', $drinkType->getName()); + $this->assertEquals('Refreshing cola drink', $drinkType->getDescription()); + $this->assertEquals(10, $drinkType->getDesiredStock()); + + // Check that it's in the database + $foundDrinkType = $this->service->getDrinkTypeByName('Cola'); + $this->assertInstanceOf(DrinkType::class, $foundDrinkType); + $this->assertEquals('Cola', $foundDrinkType->getName()); + $this->assertEquals('Refreshing cola drink', $foundDrinkType->getDescription()); + $this->assertEquals(10, $foundDrinkType->getDesiredStock()); + + // Try to create a drink type with the same name (should throw an exception) + $this->expectException(InvalidArgumentException::class); + $this->service->createDrinkType('Cola', 'Another cola drink', 5); + } + + public function testUpdateDrinkType(): void + { + // Create a drink type + $drinkType = $this->service->createDrinkType('Cola', 'Refreshing cola drink', 10); + + // Update the description + $updatedDrinkType = $this->service->updateDrinkType($drinkType, null, 'Updated description', null); + $this->assertEquals('Cola', $updatedDrinkType->getName()); + $this->assertEquals('Updated description', $updatedDrinkType->getDescription()); + $this->assertEquals(10, $updatedDrinkType->getDesiredStock()); + + // Check that it was updated in the database + $foundDrinkType = $this->service->getDrinkTypeByName('Cola'); + $this->assertEquals('Updated description', $foundDrinkType->getDescription()); + + // Update the desired stock + $updatedDrinkType = $this->service->updateDrinkType($drinkType, null, null, 20); + $this->assertEquals('Cola', $updatedDrinkType->getName()); + $this->assertEquals('Updated description', $updatedDrinkType->getDescription()); + $this->assertEquals(20, $updatedDrinkType->getDesiredStock()); + + // Check that it was updated in the database + $foundDrinkType = $this->service->getDrinkTypeByName('Cola'); + $this->assertEquals(20, $foundDrinkType->getDesiredStock()); + + // Update the name + $updatedDrinkType = $this->service->updateDrinkType($drinkType, 'Pepsi', null, null); + $this->assertEquals('Pepsi', $updatedDrinkType->getName()); + $this->assertEquals('Updated description', $updatedDrinkType->getDescription()); + $this->assertEquals(20, $updatedDrinkType->getDesiredStock()); + + // Check that it was updated in the database + $foundDrinkType = $this->service->getDrinkTypeByName('Pepsi'); + $this->assertInstanceOf(DrinkType::class, $foundDrinkType); + $this->assertEquals('Pepsi', $foundDrinkType->getName()); + $this->assertEquals('Updated description', $foundDrinkType->getDescription()); + $this->assertEquals(20, $foundDrinkType->getDesiredStock()); + + // Create another drink type + $drinkType2 = $this->service->createDrinkType('Fanta', 'Orange soda', 5); + + // Try to update the name to an existing name (should throw an exception) + $this->expectException(InvalidArgumentException::class); + $this->service->updateDrinkType($drinkType2, 'Pepsi', null, null); + } + + public function testDeleteDrinkType(): void + { + // Create a drink type + $drinkType = $this->service->createDrinkType('Cola', 'Refreshing cola drink', 10); + + // Check that it exists + $this->assertInstanceOf(DrinkType::class, $this->service->getDrinkTypeByName('Cola')); + + // Delete it + $this->service->deleteDrinkType($drinkType); + + // Check that it's gone + $this->assertNull($this->service->getDrinkTypeByName('Cola')); + } +} diff --git a/tests/Feature/Service/InventoryServiceTest.php b/tests/Feature/Service/InventoryServiceTest.php new file mode 100644 index 0000000..2e8fd3b --- /dev/null +++ b/tests/Feature/Service/InventoryServiceTest.php @@ -0,0 +1,251 @@ +setUpDB(); + $this->drinkTypeRepository = $this->container->get(DrinkTypeRepository::class); + $this->inventoryRecordRepository = $this->container->get(InventoryRecordRepository::class); + $this->configService = $this->container->get(ConfigurationService::class); + $this->service = $this->container->get(InventoryService::class); + + // Create a drink type for testing + $this->drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + $this->drinkTypeRepository->save($this->drinkType); + } + + public function testGetAllInventoryRecords(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->service->getAllInventoryRecords()); + + // Create some inventory records + $record1 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-02')); + $this->inventoryRecordRepository->save($record1); + $this->inventoryRecordRepository->save($record2); + + // Now getAllInventoryRecords should return both records + $records = $this->service->getAllInventoryRecords(); + $this->assertCount(2, $records); + $this->assertContainsOnlyInstancesOf(InventoryRecord::class, $records); + } + + public function testGetInventoryRecordsByDrinkType(): void + { + // Create another drink type + $anotherDrinkType = new DrinkType('Fanta', 'Orange soda', 5); + $this->drinkTypeRepository->save($anotherDrinkType); + + // Create inventory records for both drink types + $record1 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-02')); + $record3 = new InventoryRecord($anotherDrinkType, 15, new DateTimeImmutable('2023-01-03')); + $this->inventoryRecordRepository->save($record1); + $this->inventoryRecordRepository->save($record2); + $this->inventoryRecordRepository->save($record3); + + // Get records for the first drink type + $records = $this->service->getInventoryRecordsByDrinkType($this->drinkType); + $this->assertCount(2, $records); + $this->assertContainsOnlyInstancesOf(InventoryRecord::class, $records); + + // Check that the records are for the correct drink type + foreach ($records as $record) { + $this->assertEquals($this->drinkType->getId(), $record->getDrinkType()->getId()); + } + + // Get records for the second drink type + $records = $this->service->getInventoryRecordsByDrinkType($anotherDrinkType); + $this->assertCount(1, $records); + $this->assertEquals($anotherDrinkType->getId(), $records[0]->getDrinkType()->getId()); + } + + public function testGetLatestInventoryRecord(): void + { + // Create inventory records with different timestamps + $record1 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-02')); + $record3 = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-03')); + $this->inventoryRecordRepository->save($record1); + $this->inventoryRecordRepository->save($record2); + $this->inventoryRecordRepository->save($record3); + + // Get the latest record + $latestRecord = $this->service->getLatestInventoryRecord($this->drinkType); + $this->assertInstanceOf(InventoryRecord::class, $latestRecord); + $this->assertEquals(15, $latestRecord->getQuantity()); + $this->assertEquals('2023-01-03', $latestRecord->getTimestamp()->format('Y-m-d')); + } + + public function testGetInventoryRecordsByTimeRange(): void + { + // Create inventory records with different timestamps + $record1 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-02')); + $record3 = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-03')); + $record4 = new InventoryRecord($this->drinkType, 20, new DateTimeImmutable('2023-01-04')); + $record5 = new InventoryRecord($this->drinkType, 25, new DateTimeImmutable('2023-01-05')); + $this->inventoryRecordRepository->save($record1); + $this->inventoryRecordRepository->save($record2); + $this->inventoryRecordRepository->save($record3); + $this->inventoryRecordRepository->save($record4); + $this->inventoryRecordRepository->save($record5); + + // Get records in a specific range + $start = new DateTimeImmutable('2023-01-02'); + $end = new DateTimeImmutable('2023-01-04'); + $records = $this->service->getInventoryRecordsByTimeRange($start, $end); + + $this->assertCount(3, $records); + + // Check that the records are within the range + foreach ($records as $record) { + $timestamp = $record->getTimestamp(); + $this->assertTrue($timestamp >= $start && $timestamp <= $end); + } + } + + public function testGetCurrentStockLevel(): void + { + // Initially there should be no stock + $this->assertEquals(0, $this->service->getCurrentStockLevel($this->drinkType)); + + // Create inventory records with different timestamps + $record1 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-02')); + $record3 = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-03')); + $this->inventoryRecordRepository->save($record1); + $this->inventoryRecordRepository->save($record2); + $this->inventoryRecordRepository->save($record3); + + // The current stock level should be from the latest record + $this->assertEquals(15, $this->service->getCurrentStockLevel($this->drinkType)); + } + + public function testGetStockDifference(): void + { + // Create inventory records + $record = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-01')); + $this->inventoryRecordRepository->save($record); + + // The drink type has a desired stock of 10, and a current stock of 15 + // So the difference should be 5 (excess) + $this->assertEquals(5, $this->service->getStockDifference($this->drinkType)); + + // Create a new record with less stock + $record = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-02')); + $this->inventoryRecordRepository->save($record); + + // Now the current stock is 5, and the desired stock is 10 + // So the difference should be -5 (shortage) + $this->assertEquals(-5, $this->service->getStockDifference($this->drinkType)); + } + + public function testUpdateStockLevel(): void + { + // Update the stock level with a specific timestamp + $timestamp1 = new DateTimeImmutable('2023-01-01 10:00:00'); + $record = $this->service->updateStockLevel($this->drinkType, 15, $timestamp1); + + // Check that the record was created correctly + $this->assertInstanceOf(InventoryRecord::class, $record); + $this->assertEquals($this->drinkType->getId(), $record->getDrinkType()->getId()); + $this->assertEquals(15, $record->getQuantity()); + $this->assertEquals($timestamp1, $record->getTimestamp()); + + // Check that the current stock level is updated + $this->assertEquals(15, $this->service->getCurrentStockLevel($this->drinkType)); + + // Update the stock level again with a later timestamp + $timestamp2 = new DateTimeImmutable('2023-01-02 10:00:00'); + $record = $this->service->updateStockLevel($this->drinkType, 20, $timestamp2); + + // Check that the record was created correctly + $this->assertEquals(20, $record->getQuantity()); + $this->assertEquals($timestamp2, $record->getTimestamp()); + + // Check that the current stock level is updated to the latest record's quantity + $this->assertEquals(20, $this->service->getCurrentStockLevel($this->drinkType)); + } + + public function testGetAllDrinkTypesWithStockLevels(): void + { + // Create another drink type + $anotherDrinkType = new DrinkType('Fanta', 'Orange soda', 5); + $this->drinkTypeRepository->save($anotherDrinkType); + + // Create inventory records + $record1 = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($anotherDrinkType, 3, new DateTimeImmutable('2023-01-01')); + $this->inventoryRecordRepository->save($record1); + $this->inventoryRecordRepository->save($record2); + + // Get all drink types with stock levels + $result = $this->service->getAllDrinkTypesWithStockLevels(); + + $this->assertCount(2, $result); + + // Check the first drink type + $this->assertEquals($this->drinkType->getId(), $result[0]['drinkType']->getId()); + $this->assertEquals(15, $result[0]['currentStock']); + $this->assertEquals(10, $result[0]['desiredStock']); + $this->assertEquals(5, $result[0]['difference']); + + // Check the second drink type + $this->assertEquals($anotherDrinkType->getId(), $result[1]['drinkType']->getId()); + $this->assertEquals(3, $result[1]['currentStock']); + $this->assertEquals(5, $result[1]['desiredStock']); + $this->assertEquals(-2, $result[1]['difference']); + } + + public function testGetLowStockDrinkTypes(): void + { + // Set the low stock threshold to 70% so that 3/5 (60%) is considered low stock + $this->configService->setConfigValue(SystemSettingKey::LOW_STOCK_THRESHOLD->value, '70'); + + // Create another drink type + $anotherDrinkType = new DrinkType('Fanta', 'Orange soda', 5); + $this->drinkTypeRepository->save($anotherDrinkType); + + // Create inventory records + $record1 = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($anotherDrinkType, 3, new DateTimeImmutable('2023-01-01')); + $this->inventoryRecordRepository->save($record1); + $this->inventoryRecordRepository->save($record2); + + // Get low stock drink types + $result = $this->service->getLowStockDrinkTypes(); + + // Only the second drink type has low stock + $this->assertCount(1, $result); + $this->assertEquals($anotherDrinkType->getId(), $result[0]['drinkType']->getId()); + $this->assertEquals(3, $result[0]['currentStock']); + $this->assertEquals(5, $result[0]['desiredStock']); + $this->assertEquals(2, $result[0]['shortage']); + } +} diff --git a/tests/Feature/Service/OrderServiceTest.php b/tests/Feature/Service/OrderServiceTest.php new file mode 100644 index 0000000..d6d21e9 --- /dev/null +++ b/tests/Feature/Service/OrderServiceTest.php @@ -0,0 +1,374 @@ +setUpDB(); + $this->orderRepository = $this->container->get(OrderRepository::class); + $this->orderItemRepository = $this->container->get(OrderItemRepository::class); + $this->drinkTypeRepository = $this->container->get(DrinkTypeRepository::class); + $this->inventoryService = $this->container->get(InventoryService::class); + $this->configService = $this->container->get(ConfigurationService::class); + $this->service = $this->container->get(OrderService::class); + + // Create a drink type for testing + $this->drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + $this->drinkTypeRepository->save($this->drinkType); + } + + public function testGetAllOrders(): void + { + // Initially the repository should be empty + $this->assertCount(0, $this->service->getAllOrders()); + + // Create some orders + $order1 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + $order2 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-02')); + $this->orderRepository->save($order1); + $this->orderRepository->save($order2); + + // Now getAllOrders should return both orders + $orders = $this->service->getAllOrders(); + $this->assertCount(2, $orders); + $this->assertContainsOnlyInstancesOf(Order::class, $orders); + } + + public function testGetOrderById(): void + { + // Create an order + $order = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + $this->orderRepository->save($order); + + // Get the ID + $id = $order->getId(); + $this->assertNotNull($id); + + // Find by ID + $foundOrder = $this->service->getOrderById($id); + $this->assertInstanceOf(Order::class, $foundOrder); + $this->assertEquals(Order::STATUS_NEW, $foundOrder->getStatus()); + $this->assertEquals('2023-01-01', $foundOrder->getCreatedAt()->format('Y-m-d')); + + // Try to find a non-existent ID + $nonExistentId = 9999; + $this->assertNull($this->service->getOrderById($nonExistentId)); + } + + public function testGetOrdersByStatus(): void + { + // Create orders with different statuses + $order1 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + $order2 = new Order(Order::STATUS_IN_WORK, new DateTimeImmutable('2023-01-02')); + $order3 = new Order(Order::STATUS_ORDERED, new DateTimeImmutable('2023-01-03')); + $order4 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-04')); + $order5 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-05')); + $this->orderRepository->save($order1); + $this->orderRepository->save($order2); + $this->orderRepository->save($order3); + $this->orderRepository->save($order4); + $this->orderRepository->save($order5); + + // Get orders by status + $newOrders = $this->service->getOrdersByStatus(Order::STATUS_NEW); + $this->assertCount(2, $newOrders); + $this->assertContainsOnlyInstancesOf(Order::class, $newOrders); + + // Check that the orders have the correct status + foreach ($newOrders as $order) { + $this->assertEquals(Order::STATUS_NEW, $order->getStatus()); + } + + // Get orders by another status + $fulfilledOrders = $this->service->getOrdersByStatus(Order::STATUS_FULFILLED); + $this->assertCount(1, $fulfilledOrders); + $this->assertEquals(Order::STATUS_FULFILLED, $fulfilledOrders[0]->getStatus()); + } + + public function testGetOrdersByDateRange(): void + { + // Create orders with different dates + $order1 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-01')); + $order2 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-02')); + $order3 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-03')); + $order4 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-04')); + $order5 = new Order(Order::STATUS_NEW, new DateTimeImmutable('2023-01-05')); + $this->orderRepository->save($order1); + $this->orderRepository->save($order2); + $this->orderRepository->save($order3); + $this->orderRepository->save($order4); + $this->orderRepository->save($order5); + + // Get orders in a specific date range + $start = new DateTimeImmutable('2023-01-02'); + $end = new DateTimeImmutable('2023-01-04'); + $orders = $this->service->getOrdersByDateRange($start, $end); + + $this->assertCount(3, $orders); + + // Check that the orders are within the date range + foreach ($orders as $order) { + $createdAt = $order->getCreatedAt(); + $this->assertTrue($createdAt >= $start && $createdAt <= $end); + } + } + + public function testCreateOrder(): void + { + // Create an order with items + $items = [ + ['drinkTypeId' => $this->drinkType->getId(), 'quantity' => 5], + ]; + $order = $this->service->createOrder($items); + + // Check that the order was created correctly + $this->assertInstanceOf(Order::class, $order); + $this->assertEquals(Order::STATUS_NEW, $order->getStatus()); + + // Refresh the order from the database + $refreshedOrder = $this->service->getOrderById($order->getId()); + $this->assertInstanceOf(Order::class, $refreshedOrder); + + // Check that the order items were created correctly + $orderItems = $refreshedOrder->getOrderItems(); + $this->assertCount(1, $orderItems); + $orderItem = $orderItems->first(); + $this->assertEquals($this->drinkType->getId(), $orderItem->getDrinkType()->getId()); + $this->assertEquals(5, $orderItem->getQuantity()); + + // Try to create an order with an invalid drink type ID + $this->expectException(InvalidArgumentException::class); + $this->service->createOrder([['drinkTypeId' => 9999, 'quantity' => 5]]); + } + + public function testCreateOrderFromStockLevels(): void + { + // Set the low stock threshold to 70% so that 3/5 (60%) is considered low stock + $this->configService->setConfigValue(SystemSettingKey::LOW_STOCK_THRESHOLD->value, '70'); + + // Create a drink type with low stock + $drinkType = new DrinkType('Fanta', 'Orange soda', 5); + $this->drinkTypeRepository->save($drinkType); + + // Create inventory records + $this->inventoryService->updateStockLevel($this->drinkType, 15); // Excess stock + $this->inventoryService->updateStockLevel($drinkType, 3); // Low stock + + // Create an order from stock levels + $order = $this->service->createOrderFromStockLevels(); + + // Check that the order was created correctly + $this->assertInstanceOf(Order::class, $order); + $this->assertEquals(Order::STATUS_NEW, $order->getStatus()); + + // Refresh the order from the database + $refreshedOrder = $this->service->getOrderById($order->getId()); + $this->assertInstanceOf(Order::class, $refreshedOrder); + + // Check that the order items were created correctly + $orderItems = $refreshedOrder->getOrderItems(); + $this->assertCount(1, $orderItems); // Only one drink type has low stock + $orderItem = $orderItems->first(); + $this->assertEquals($drinkType->getId(), $orderItem->getDrinkType()->getId()); + $this->assertEquals(2, $orderItem->getQuantity()); // Shortage is 2 + } + + public function testUpdateOrderStatus(): void + { + // Create an order + $order = new Order(Order::STATUS_NEW); + $this->orderRepository->save($order); + + // Update the status + $updatedOrder = $this->service->updateOrderStatus($order, Order::STATUS_IN_WORK); + $this->assertEquals(Order::STATUS_IN_WORK, $updatedOrder->getStatus()); + + // Check that it was updated in the database + $foundOrder = $this->service->getOrderById($order->getId()); + $this->assertEquals(Order::STATUS_IN_WORK, $foundOrder->getStatus()); + + // Update to fulfilled status + $orderItem = new OrderItem($this->drinkType, 5, $foundOrder); + $this->orderItemRepository->save($orderItem); + + // Refresh the order from the database + $refreshedOrder = $this->service->getOrderById($foundOrder->getId()); + $this->assertInstanceOf(Order::class, $refreshedOrder); + $this->assertCount(1, $refreshedOrder->getOrderItems()); + + // Create an initial inventory record with a specific timestamp + $timestamp1 = new DateTimeImmutable('2023-01-01 10:00:00'); + $this->inventoryService->updateStockLevel($this->drinkType, 10, $timestamp1); + + // Update the status to fulfilled + $updatedOrder = $this->service->updateOrderStatus($refreshedOrder, Order::STATUS_FULFILLED); + $this->assertEquals(Order::STATUS_FULFILLED, $updatedOrder->getStatus()); + + // Check that the inventory was updated with a new record that has a later timestamp + $currentStock = $this->inventoryService->getCurrentStockLevel($this->drinkType); + $this->assertEquals(15, $currentStock); // 10 + 5 = 15 + + // Try to update to an invalid status + $this->expectException(InvalidArgumentException::class); + $this->service->updateOrderStatus($updatedOrder, 'invalid_status'); + } + + public function testAddOrderItem(): void + { + // Create an order + $order = new Order(Order::STATUS_NEW); + $this->orderRepository->save($order); + + // Add an item + $orderItem = $this->service->addOrderItem($order, $this->drinkType, 5); + + // Check that the item was added correctly + $this->assertInstanceOf(OrderItem::class, $orderItem); + $this->assertEquals($this->drinkType->getId(), $orderItem->getDrinkType()->getId()); + $this->assertEquals(5, $orderItem->getQuantity()); + $this->assertEquals($order->getId(), $orderItem->getOrder()->getId()); + + // Refresh the order from the database + $refreshedOrder = $this->service->getOrderById($order->getId()); + $this->assertInstanceOf(Order::class, $refreshedOrder); + + // Check that the item is in the order + $orderItems = $refreshedOrder->getOrderItems(); + $this->assertCount(1, $orderItems); + $this->assertEquals($orderItem->getId(), $orderItems->first()->getId()); + + // Add another item for the same drink type + $orderItem2 = $this->service->addOrderItem($refreshedOrder, $this->drinkType, 3); + + // Check that the quantity was updated + $this->assertEquals(8, $orderItem2->getQuantity()); // 5 + 3 = 8 + + // Refresh the order again + $refreshedOrder = $this->service->getOrderById($refreshedOrder->getId()); + + // Check that there's still only one item in the order + $orderItems = $refreshedOrder->getOrderItems(); + $this->assertCount(1, $orderItems); + + // Create another drink type + $anotherDrinkType = new DrinkType('Fanta', 'Orange soda', 5); + $this->drinkTypeRepository->save($anotherDrinkType); + + // Add an item for the new drink type + $orderItem3 = $this->service->addOrderItem($refreshedOrder, $anotherDrinkType, 2); + + // Check that the item was added correctly + $this->assertInstanceOf(OrderItem::class, $orderItem3); + $this->assertEquals($anotherDrinkType->getId(), $orderItem3->getDrinkType()->getId()); + $this->assertEquals(2, $orderItem3->getQuantity()); + + // Refresh the order again + $refreshedOrder = $this->service->getOrderById($refreshedOrder->getId()); + + // Check that there are now two items in the order + $orderItems = $refreshedOrder->getOrderItems(); + $this->assertCount(2, $orderItems); + + // Try to add an item to an order that's not in 'new' or 'in_work' status + $refreshedOrder->setStatus(Order::STATUS_ORDERED); + $this->orderRepository->save($refreshedOrder); + + $this->expectException(InvalidArgumentException::class); + $this->service->addOrderItem($refreshedOrder, $this->drinkType, 1); + } + + public function testRemoveOrderItem(): void + { + // Create an order + $order = new Order(Order::STATUS_NEW); + $this->orderRepository->save($order); + + // Add an item + $orderItem = $this->service->addOrderItem($order, $this->drinkType, 5); + + // Refresh the order from the database + $refreshedOrder = $this->service->getOrderById($order->getId()); + $this->assertInstanceOf(Order::class, $refreshedOrder); + + // Check that the item is in the order + $orderItems = $refreshedOrder->getOrderItems(); + $this->assertCount(1, $orderItems); + $this->assertEquals($orderItem->getId(), $orderItems->first()->getId()); + + // Remove the item + $this->service->removeOrderItem($refreshedOrder, $orderItem); + + // Refresh the order again + $refreshedOrder = $this->service->getOrderById($refreshedOrder->getId()); + + // Check that the item is gone + $orderItems = $refreshedOrder->getOrderItems(); + $this->assertCount(0, $orderItems); + + // Try to remove an item from an order that's not in 'new' or 'in_work' status + $refreshedOrder->setStatus(Order::STATUS_ORDERED); + $this->orderRepository->save($refreshedOrder); + + $orderItem = new OrderItem($this->drinkType, 5, $refreshedOrder); + $this->orderItemRepository->save($orderItem); + + $this->expectException(InvalidArgumentException::class); + $this->service->removeOrderItem($refreshedOrder, $orderItem); + } + + public function testDeleteOrder(): void + { + // Create an order + $order = new Order(Order::STATUS_NEW); + $this->orderRepository->save($order); + + // Get the ID + $id = $order->getId(); + $this->assertNotNull($id); + + // Check that it exists + $this->assertInstanceOf(Order::class, $this->service->getOrderById($id)); + + // Delete it + $this->service->deleteOrder($order); + + // Check that it's gone + $this->assertNull($this->service->getOrderById($id)); + + // Try to delete an order that's not in 'new' status + $order = new Order(Order::STATUS_IN_WORK); + $this->orderRepository->save($order); + + $this->expectException(InvalidArgumentException::class); + $this->service->deleteOrder($order); + } +} diff --git a/tests/Feature/Service/StockAdjustmentServiceTest.php b/tests/Feature/Service/StockAdjustmentServiceTest.php new file mode 100644 index 0000000..6e8e986 --- /dev/null +++ b/tests/Feature/Service/StockAdjustmentServiceTest.php @@ -0,0 +1,203 @@ +setUpDB(); + $this->drinkTypeRepository = $this->container->get(DrinkTypeRepository::class); + $this->inventoryRecordRepository = $this->container->get(InventoryRecordRepository::class); + $this->orderRepository = $this->container->get(OrderRepository::class); + $this->orderItemRepository = $this->container->get(OrderItemRepository::class); + $this->configService = $this->container->get(ConfigurationService::class); + $this->inventoryService = $this->container->get(InventoryService::class); + $this->service = $this->container->get(StockAdjustmentService::class); + + // Initialize default configs + $this->configService->initializeDefaultConfigs(); + + // Create a drink type for testing + $this->drinkType = new DrinkType('Cola', 'Refreshing cola drink', 10); + $this->drinkTypeRepository->save($this->drinkType); + } + + public function testAdjustStockLevels(): void + { + // Create orders with different dates + $order1 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-01')); + $order2 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-02')); + $order3 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-03')); + $order4 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-04')); + $order5 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-05')); + $this->orderRepository->save($order1); + $this->orderRepository->save($order2); + $this->orderRepository->save($order3); + $this->orderRepository->save($order4); + $this->orderRepository->save($order5); + + // Create order items for the drink type with different quantities + $orderItem1 = new OrderItem($this->drinkType, 5, $order1); + $orderItem2 = new OrderItem($this->drinkType, 10, $order2); + $orderItem3 = new OrderItem($this->drinkType, 15, $order3); + $orderItem4 = new OrderItem($this->drinkType, 20, $order4); + $orderItem5 = new OrderItem($this->drinkType, 25, $order5); + $this->orderItemRepository->save($orderItem1); + $this->orderItemRepository->save($orderItem2); + $this->orderItemRepository->save($orderItem3); + $this->orderItemRepository->save($orderItem4); + $this->orderItemRepository->save($orderItem5); + + // Set the desired stock to a value that will trigger an adjustment + $this->drinkType->setDesiredStock(5); + $this->drinkTypeRepository->save($this->drinkType); + + // Adjust stock levels + $result = $this->service->adjustStockLevels(); + + // Check that the adjustment was made + $this->assertCount(1, $result); + $this->assertEquals($this->drinkType->getId(), $result[0]['drinkType']->getId()); + $this->assertEquals(5, $result[0]['oldDesiredStock']); + + // The new desired stock should be higher than the old one + $this->assertGreaterThan(5, $result[0]['newDesiredStock']); + + // Check that the drink type was updated in the database + $updatedDrinkType = $this->drinkTypeRepository->find($this->drinkType->getId()); + $this->assertEquals($result[0]['newDesiredStock'], $updatedDrinkType->getDesiredStock()); + } + + public function testAdjustStockLevelsWithNoOrders(): void + { + // No orders, so no adjustment should be made + $result = $this->service->adjustStockLevels(); + + // Check that no adjustment was made + $this->assertCount(0, $result); + + // Check that the drink type was not updated + $updatedDrinkType = $this->drinkTypeRepository->find($this->drinkType->getId()); + $this->assertEquals(10, $updatedDrinkType->getDesiredStock()); + } + + public function testAdjustStockLevelsWithSmallDifference(): void + { + // Create orders with different dates + $order1 = new Order(Order::STATUS_FULFILLED, new DateTimeImmutable('2023-01-01')); + $this->orderRepository->save($order1); + + // Create order items for the drink type with a quantity close to the desired stock + $orderItem1 = new OrderItem($this->drinkType, 11, $order1); + $this->orderItemRepository->save($orderItem1); + + // Set the threshold to a high value so that small differences don't trigger an adjustment + $this->configService->setConfigValue(SystemSettingKey::STOCK_ADJUSTMENT_THRESHOLD->value, '0.5'); + + // Adjust stock levels + $result = $this->service->adjustStockLevels(); + + // Check that no adjustment was made because the difference is too small + $this->assertCount(0, $result); + + // Check that the drink type was not updated + $updatedDrinkType = $this->drinkTypeRepository->find($this->drinkType->getId()); + $this->assertEquals(10, $updatedDrinkType->getDesiredStock()); + } + + public function testGetConsumptionHistory(): void + { + // Create inventory records with different timestamps + $record1 = new InventoryRecord($this->drinkType, 20, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-02')); // Consumption: 5 + $record3 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-03')); // Consumption: 5 + $record4 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-04')); // Consumption: 5 + $record5 = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-05')); // No consumption (increase) + $this->inventoryRecordRepository->save($record1); + $this->inventoryRecordRepository->save($record2); + $this->inventoryRecordRepository->save($record3); + $this->inventoryRecordRepository->save($record4); + $this->inventoryRecordRepository->save($record5); + + // Get consumption history + $start = new DateTimeImmutable('2023-01-01'); + $end = new DateTimeImmutable('2023-01-05'); + $history = $this->service->getConsumptionHistory($this->drinkType, $start, $end); + + // Check that the history is correct + $this->assertCount(3, $history); // 3 consumption events + + // Check the first consumption event + $this->assertEquals('2023-01-02', $history[0]['date']->format('Y-m-d')); + $this->assertEquals(5, $history[0]['consumption']); + + // Check the second consumption event + $this->assertEquals('2023-01-03', $history[1]['date']->format('Y-m-d')); + $this->assertEquals(5, $history[1]['consumption']); + + // Check the third consumption event + $this->assertEquals('2023-01-04', $history[2]['date']->format('Y-m-d')); + $this->assertEquals(5, $history[2]['consumption']); + } + + public function testGetConsumptionHistoryWithDateRange(): void + { + // Create inventory records with different timestamps + $record1 = new InventoryRecord($this->drinkType, 20, new DateTimeImmutable('2023-01-01')); + $record2 = new InventoryRecord($this->drinkType, 15, new DateTimeImmutable('2023-01-02')); // Consumption: 5 + $record3 = new InventoryRecord($this->drinkType, 10, new DateTimeImmutable('2023-01-03')); // Consumption: 5 + $record4 = new InventoryRecord($this->drinkType, 5, new DateTimeImmutable('2023-01-04')); // Consumption: 5 + $record5 = new InventoryRecord($this->drinkType, 0, new DateTimeImmutable('2023-01-05')); // Consumption: 5 + $this->inventoryRecordRepository->save($record1); + $this->inventoryRecordRepository->save($record2); + $this->inventoryRecordRepository->save($record3); + $this->inventoryRecordRepository->save($record4); + $this->inventoryRecordRepository->save($record5); + + // Get consumption history for a specific date range + $start = new DateTimeImmutable('2023-01-02'); + $end = new DateTimeImmutable('2023-01-04'); + $history = $this->service->getConsumptionHistory($this->drinkType, $start, $end); + + // Check that the history is correct + $this->assertCount(2, $history); // 2 consumption events within the range + + // Check the first consumption event + $this->assertEquals('2023-01-03', $history[0]['date']->format('Y-m-d')); + $this->assertEquals(5, $history[0]['consumption']); + + // Check the second consumption event + $this->assertEquals('2023-01-04', $history[1]['date']->format('Y-m-d')); + $this->assertEquals(5, $history[1]['consumption']); + } +} diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..7b15ab4 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,7 @@ +extend(TestCase::class)->in('Feature'); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..f05f754 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,35 @@ +container = (require __DIR__ . '/../config/container.php')(new Settings(true)); + $this->app = Bridge::create($this->container); + } + + public function setUpDB(): void + { + $entityManager = $this->container->get(EntityManagerInterface::class); + + // Create schema from entities + $metadata = $entityManager->getMetadataFactory()->getAllMetadata(); + $tool = new SchemaTool($entityManager); + $tool->dropSchema($metadata); + $tool->createSchema($metadata); + } +}