diff --git a/app/config/bundles.php b/app/config/bundles.php index cc162bd69..99470090d 100644 --- a/app/config/bundles.php +++ b/app/config/bundles.php @@ -18,4 +18,5 @@ Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], CuyZ\ValinorBundle\ValinorBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], ]; diff --git a/app/config/packages/doctrine.yaml b/app/config/packages/doctrine.yaml new file mode 100644 index 000000000..757d04787 --- /dev/null +++ b/app/config/packages/doctrine.yaml @@ -0,0 +1,57 @@ +doctrine: + dbal: + dbname: '%env(DATABASE_NAME)%' + host: '%env(DATABASE_HOST)%' + port: '%env(DATABASE_PORT)%' + user: '%env(DATABASE_USER)%' + password: '%env(DATABASE_PASSWORD)%' + server_version: '5.7' + + profiling_collect_backtrace: '%kernel.debug%' + use_savepoints: true + orm: + auto_generate_proxy_classes: true + enable_lazy_ghost_objects: true + report_fields_where_declared: true + validate_xml_mapping: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + controller_resolver: + auto_mapping: false + mappings: + Accounting: + type: attribute + is_bundle: false + dir: '%kernel.project_dir%/../sources/AppBundle/Accounting/Entity' + prefix: 'AppBundle\Accounting\Entity' + alias: Accounting + +when@test: + doctrine: + dbal: + dbname: 'web' + host: 'dbtest' + port: '3306' + user: 'root' + password: 'root' + server_version: '5.7' + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/composer.json b/composer.json index e553cffaa..0f4295cd3 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,8 @@ "cuyz/valinor-bundle": "^2.0", "cweagans/composer-patches": "^1.7", "doctrine/dbal": "4.*", + "doctrine/doctrine-bundle": "^2.18", + "doctrine/orm": "^3.5", "drewm/mailchimp-api": "^2.5", "ekino/newrelic-bundle": "dev-patch-1#2cd9951c163bda2d18a1515b43ee574e51aac871", "erusev/parsedown": "^1.6", @@ -145,6 +147,7 @@ "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.1", "phpstan/phpstan-beberlei-assert": "^2.0", + "phpstan/phpstan-doctrine": "^2.0", "phpstan/phpstan-symfony": "^2.0", "phpunit/phpunit": "11.*", "rector/rector": "^2.0", diff --git a/composer.lock b/composer.lock index 2bf8f5166..ea7b4eeb4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "68baf43f99dadc0cef165264170e6131", + "content-hash": "a1569f89bcc41e4e9b3bee1f4f7e29c5", "packages": [ { "name": "algolia/algoliasearch-client-php", @@ -1137,6 +1137,92 @@ ], "time": "2022-05-20T20:06:54+00:00" }, + { + "name": "doctrine/collections", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "9acfeea2e8666536edff3d77c531261c63680160" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/9acfeea2e8666536edff3d77c531261c63680160", + "reference": "9acfeea2e8666536edff3d77c531261c63680160", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "ext-json": "*", + "phpstan/phpstan": "^2.1.30", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4" + }, + "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.4.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-10-25T09:18:13+00:00" + }, { "name": "doctrine/dbal", "version": "4.3.4", @@ -1291,6 +1377,308 @@ }, "time": "2025-04-07T20:06:18+00:00" }, + { + "name": "doctrine/doctrine-bundle", + "version": "2.18.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "b769877014de053da0e5cbbb63d0ea2f3b2fea76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/b769877014de053da0e5cbbb63d0ea2f3b2fea76", + "reference": "b769877014de053da0e5cbbb63d0ea2f3b2fea76", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^3.7.0 || ^4.0", + "doctrine/deprecations": "^1.0", + "doctrine/persistence": "^3.1 || ^4", + "doctrine/sql-formatter": "^1.0.1", + "php": "^8.1", + "symfony/cache": "^6.4 || ^7.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/service-contracts": "^2.5 || ^3" + }, + "conflict": { + "doctrine/annotations": ">=3.0", + "doctrine/cache": "< 1.11", + "doctrine/orm": "<2.17 || >=4.0", + "symfony/var-exporter": "< 6.4.1 || 7.0.0", + "twig/twig": "<2.13 || >=3.0 <3.0.4" + }, + "require-dev": { + "doctrine/annotations": "^1 || ^2", + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^14", + "doctrine/orm": "^2.17 || ^3.1", + "friendsofphp/proxy-manager-lts": "^1.0", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.53 || ^12.3.10", + "psr/log": "^1.1.4 || ^2.0 || ^3.0", + "symfony/doctrine-messenger": "^6.4 || ^7.0", + "symfony/expression-language": "^6.4 || ^7.0", + "symfony/messenger": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/stopwatch": "^6.4 || ^7.0", + "symfony/string": "^6.4 || ^7.0", + "symfony/twig-bridge": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "symfony/var-exporter": "^6.4.1 || ^7.0.1", + "symfony/web-profiler-bundle": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0", + "twig/twig": "^2.14.7 || ^3.0.4" + }, + "suggest": { + "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", + "ext-pdo": "*", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org/" + } + ], + "description": "Symfony DoctrineBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineBundle/issues", + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle", + "type": "tidelift" + } + ], + "time": "2025-11-05T14:42:10+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.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "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 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.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, { "name": "doctrine/instantiator", "version": "2.0.0", @@ -1438,6 +1826,242 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "doctrine/orm", + "version": "3.5.8", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "78dd074266e8b47a83bcf60ab5fe06c91a639168" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/78dd074266e8b47a83bcf60ab5fe06c91a639168", + "reference": "78dd074266e8b47a83bcf60ab5fe06c91a639168", + "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 || ^8.0", + "symfony/var-exporter": "^6.3.9 || ^7.0 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "2.1.23", + "phpstan/phpstan-deprecation-rules": "^2", + "phpunit/phpunit": "^10.5.0 || ^11.5", + "psr/log": "^1 || ^2 || ^3", + "symfony/cache": "^5.4 || ^6.2 || ^7.0 || ^8.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.5.8" + }, + "time": "2025-11-29T23:11:02+00:00" + }, + { + "name": "doctrine/persistence", + "version": "4.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/b9c49ad3558bb77ef973f4e173f2e9c2eca9be09", + "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1 || ^2", + "php": "^8.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.58 || ^12", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^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.1.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2025-10-16T20:13:18+00:00" + }, + { + "name": "doctrine/sql-formatter", + "version": "1.5.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/sql-formatter.git", + "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/a8af23a8e9d622505baa2997465782cbe8bb7fc7", + "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "ergebnis/phpunit-slow-test-detector": "^2.20", + "phpstan/phpstan": "^2.1.31", + "phpunit/phpunit": "^10.5.58" + }, + "bin": [ + "bin/sql-formatter" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\SqlFormatter\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Dorn", + "email": "jeremy@jeremydorn.com", + "homepage": "https://jeremydorn.com/" + } + ], + "description": "a PHP SQL highlighting library", + "homepage": "https://github.com/doctrine/sql-formatter/", + "keywords": [ + "highlight", + "sql" + ], + "support": { + "issues": "https://github.com/doctrine/sql-formatter/issues", + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.3" + }, + "time": "2025-10-26T09:35:14+00:00" + }, { "name": "drewm/mailchimp-api", "version": "v2.5.4", @@ -5454,19 +6078,128 @@ "require": { "php": ">=8.1" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } + "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/doctrine-bridge", + "version": "v7.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "e7d308bd44ff8673a259e2727d13af6a93a5d83e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/e7d308bd44ff8673a259e2727d13af6a93a5d83e", + "reference": "e7d308bd44ff8673a259e2727d13af6a93a5d83e", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^2", + "doctrine/persistence": "^3.1|^4", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/collections": "<1.8", + "doctrine/dbal": "<3.6", + "doctrine/lexer": "<1.1", + "doctrine/orm": "<2.15", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/form": "<6.4.6|>=7,<7.0.6", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/lock": "<6.4", + "symfony/messenger": "<6.4", + "symfony/property-info": "<6.4", + "symfony/security-bundle": "<6.4", + "symfony/security-core": "<6.4", + "symfony/validator": "<6.4" + }, + "require-dev": { + "doctrine/collections": "^1.8|^2.0", + "doctrine/data-fixtures": "^1.1|^2", + "doctrine/dbal": "^3.6|^4", + "doctrine/orm": "^2.15|^3", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/doctrine-messenger": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/form": "^6.4.6|^7.0.6", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/type-info": "^7.1.8", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, + "type": "symfony-bridge", "autoload": { - "files": [ - "function.php" + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5475,18 +6208,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "A generic function and convention to trigger deprecation notices", + "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/doctrine-bridge/tree/v7.3.5" }, "funding": [ { @@ -5497,12 +6230,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-09-27T09:00:46+00:00" }, { "name": "symfony/error-handler", @@ -7745,6 +8482,86 @@ ], "time": "2025-07-08T02:45:35+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "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.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, { "name": "symfony/property-access", "version": "v7.3.3", @@ -11282,6 +12099,79 @@ }, "time": "2025-10-06T09:51:30+00:00" }, + { + "name": "phpstan/phpstan-doctrine", + "version": "2.0.12", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-doctrine.git", + "reference": "d20ee0373d22735271f1eb4d631856b5f847d399" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/d20ee0373d22735271f1eb4d631856b5f847d399", + "reference": "d20ee0373d22735271f1eb4d631856b5f847d399", + "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.8", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6.20", + "ramsey/uuid": "^4.2", + "symfony/cache": "^5.4", + "symfony/uid": "^5.4 || ^6.4 || ^7.3" + }, + "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.12" + }, + "time": "2025-12-01T11:34:02+00:00" + }, { "name": "phpstan/phpstan-symfony", "version": "2.0.8", @@ -13831,86 +14721,6 @@ ], "time": "2025-09-27T15:48:31+00:00" }, - { - "name": "symfony/polyfill-php84", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", - "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.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-06-24T13:30:11+00:00" - }, { "name": "symfony/process", "version": "v7.3.4", diff --git a/phpstan.neon b/phpstan.neon index 18ff6c3b9..4963981f1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -18,7 +18,11 @@ parameters: property: 45 # since PHP 8.3 # constant: 30 + doctrine: + ormRepositoryClass: AppBundle\Doctrine\EntityRepository rules: - AppBundle\StaticAnalysis\Rule\NoDebugFunctionsRule - AppBundle\StaticAnalysis\Rule\NoGetUserMethodRule + - AppBundle\StaticAnalysis\Rule\DoctrineRepositoryRule + - AppBundle\StaticAnalysis\Rule\DoctrineDisableDQLRule diff --git a/sources/AppBundle/Accounting/Entity/Repository/RuleRepository.php b/sources/AppBundle/Accounting/Entity/Repository/RuleRepository.php new file mode 100644 index 000000000..dc1cb56d3 --- /dev/null +++ b/sources/AppBundle/Accounting/Entity/Repository/RuleRepository.php @@ -0,0 +1,31 @@ + + */ +final class RuleRepository extends EntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Rule::class); + } + + /** + * @return array + */ + public function getAllSortedByName(): array + { + return $this->createQueryBuilder('r') + ->orderBy('r.label', 'asc') + ->getQuery() + ->execute(); + } +} diff --git a/sources/AppBundle/Accounting/Entity/Rule.php b/sources/AppBundle/Accounting/Entity/Rule.php new file mode 100644 index 000000000..97ee71cb5 --- /dev/null +++ b/sources/AppBundle/Accounting/Entity/Rule.php @@ -0,0 +1,41 @@ + - */ -class RuleRepository extends Repository implements MetadataInitializer -{ - /** - * @return CollectionInterface - */ - public function getAllSortedByName(): CollectionInterface - { - $query = $this->getQuery('SELECT * FROM compta_regle ORDER BY label asc'); - return $query->query($this->getCollection(new HydratorSingleObject())); - } - - public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) - { - $metadata = new Metadata($serializerFactory); - - $metadata->setEntity(Rule::class); - $metadata->setConnectionName('main'); - $metadata->setDatabase($options['database']); - $metadata->setTable('compta_regle'); - - $metadata - ->addField([ - 'columnName' => 'id', - 'fieldName' => 'id', - 'primary' => true, - 'autoincrement' => true, - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'label', - 'fieldName' => 'label', - 'type' => 'string', - ]) - ->addField([ - 'columnName' => 'condition', - 'fieldName' => 'condition', - 'type' => 'string', - ]) - ->addField([ - 'columnName' => 'is_credit', - 'fieldName' => 'isCredit', - 'type' => 'bool', - 'serializer' => Boolean::class, - ]) - ->addField([ - 'columnName' => 'mode_regl_id', - 'fieldName' => 'paymentTypeId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'vat', - 'fieldName' => 'vat', - 'type' => 'string', - ]) - ->addField([ - 'columnName' => 'category_id', - 'fieldName' => 'categoryId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'event_id', - 'fieldName' => 'eventId', - 'type' => 'int', - ]) - ->addField([ - 'columnName' => 'attachment_required', - 'fieldName' => 'attachmentRequired', - 'type' => 'bool', - 'serializer' => Boolean::class, - ]) - ; - - return $metadata; - } -} diff --git a/sources/AppBundle/Accounting/Model/Rule.php b/sources/AppBundle/Accounting/Model/Rule.php deleted file mode 100644 index b805fa64d..000000000 --- a/sources/AppBundle/Accounting/Model/Rule.php +++ /dev/null @@ -1,140 +0,0 @@ -id; - } - - public function setId(int $id): self - { - $this->propertyChanged('id', $this->id, $id); - $this->id = $id; - return $this; - } - - public function getLabel(): ?string - { - return $this->label; - } - - public function setLabel(?string $label): self - { - $this->propertyChanged('label', $this->label, $label); - $this->label = $label; - - return $this; - } - - public function getCondition(): ?string - { - return $this->condition; - } - - public function setCondition(?string $condition): self - { - $this->propertyChanged('condition', $this->condition, $condition); - $this->condition = $condition; - - return $this; - } - - public function getIsCredit(): ?bool - { - return $this->isCredit; - } - - public function setIsCredit(?bool $isCredit): self - { - $this->propertyChanged('isCredit', $this->isCredit, $isCredit); - $this->isCredit = $isCredit; - - return $this; - } - - public function getpaymentTypeId(): ?int - { - return $this->paymentTypeId; - } - - public function setPaymentTypeId(?int $paymentTypeId): void - { - $this->propertyChanged('paymentTypeId', $this->paymentTypeId, $paymentTypeId); - $this->paymentTypeId = $paymentTypeId; - } - - public function getVat(): ?string - { - return $this->vat; - } - - public function setVat(?string $vat): self - { - $this->propertyChanged('vat', $this->vat, $vat); - $this->vat = $vat; - - return $this; - } - - public function getCategoryId(): ?int - { - return $this->categoryId; - } - - public function setCategoryId(?int $categoryId): void - { - $this->propertyChanged('categoryId', $this->categoryId, $categoryId); - $this->categoryId = $categoryId; - } - - public function getEventId(): ?int - { - return $this->eventId; - } - - public function setEventId(?int $eventId): void - { - $this->propertyChanged('eventId', $this->eventId, $eventId); - $this->eventId = $eventId; - } - - public function IsAttachmentRequired(): ?bool - { - return $this->attachmentRequired; - } - - public function setAttachmentRequired(?bool $attachmentRequired): self - { - $this->propertyChanged('attachmentRequired', $this->attachmentRequired, $attachmentRequired); - $this->attachmentRequired = $attachmentRequired; - - return $this; - } -} diff --git a/sources/AppBundle/Controller/Admin/Accounting/Configuration/AddRuleAction.php b/sources/AppBundle/Controller/Admin/Accounting/Configuration/AddRuleAction.php index 2d0d77482..3b0f96a8f 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Configuration/AddRuleAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Configuration/AddRuleAction.php @@ -4,9 +4,9 @@ namespace AppBundle\Controller\Admin\Accounting\Configuration; +use AppBundle\Accounting\Entity\Repository\RuleRepository; +use AppBundle\Accounting\Entity\Rule; use AppBundle\Accounting\Form\RuleType; -use AppBundle\Accounting\Model\Rule; -use AppBundle\Accounting\Model\Repository\RuleRepository; use AppBundle\AuditLog\Audit; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -26,7 +26,7 @@ public function __invoke(Request $request): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->ruleRepository->save($rule); - $this->audit->log('Ajout de la règle ' . $rule->getLabel()); + $this->audit->log('Ajout de la règle ' . $rule->label); $this->addFlash('notice', 'La règle a été ajoutée'); return $this->redirectToRoute('admin_accounting_rules_list'); } diff --git a/sources/AppBundle/Controller/Admin/Accounting/Configuration/EditRuleAction.php b/sources/AppBundle/Controller/Admin/Accounting/Configuration/EditRuleAction.php index a886aabea..210291e19 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Configuration/EditRuleAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Configuration/EditRuleAction.php @@ -5,7 +5,7 @@ namespace AppBundle\Controller\Admin\Accounting\Configuration; use AppBundle\Accounting\Form\RuleType; -use AppBundle\Accounting\Model\Repository\RuleRepository; +use AppBundle\Accounting\Entity\Repository\RuleRepository; use AppBundle\AuditLog\Audit; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -20,12 +20,12 @@ public function __construct( public function __invoke(int $id,Request $request): Response { - $rule = $this->ruleRepository->get($id); + $rule = $this->ruleRepository->find($id); $form = $this->createForm(RuleType::class, $rule); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->ruleRepository->save($rule); - $this->audit->log('Modification de la règle ' . $rule->getLabel()); + $this->audit->log('Modification de la règle ' . $rule->label); $this->addFlash('notice', 'La règle a été modifiée'); return $this->redirectToRoute('admin_accounting_rules_list'); } diff --git a/sources/AppBundle/Controller/Admin/Accounting/Configuration/ListRuleAction.php b/sources/AppBundle/Controller/Admin/Accounting/Configuration/ListRuleAction.php index 0cb4390ea..a36888844 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Configuration/ListRuleAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Configuration/ListRuleAction.php @@ -4,7 +4,7 @@ namespace AppBundle\Controller\Admin\Accounting\Configuration; -use AppBundle\Accounting\Model\Repository\RuleRepository; +use AppBundle\Accounting\Entity\Repository\RuleRepository; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Twig\Environment; diff --git a/sources/AppBundle/Doctrine/EntityRepository.php b/sources/AppBundle/Doctrine/EntityRepository.php new file mode 100644 index 000000000..f9fc8d190 --- /dev/null +++ b/sources/AppBundle/Doctrine/EntityRepository.php @@ -0,0 +1,32 @@ + + */ +abstract class EntityRepository extends ServiceEntityRepository +{ + /** + * @param T $entity + */ + final public function save(object $entity): void + { + $this->getEntityManager()->persist($entity); + $this->getEntityManager()->flush(); + } + + /** + * @param T $entity + */ + final public function delete(object $entity): void + { + $this->getEntityManager()->remove($entity); + $this->getEntityManager()->flush(); + } +} diff --git a/sources/AppBundle/StaticAnalysis/Rule/DoctrineDisableDQLRule.php b/sources/AppBundle/StaticAnalysis/Rule/DoctrineDisableDQLRule.php new file mode 100644 index 000000000..ea5eb0160 --- /dev/null +++ b/sources/AppBundle/StaticAnalysis/Rule/DoctrineDisableDQLRule.php @@ -0,0 +1,71 @@ + + */ +final readonly class DoctrineDisableDQLRule implements Rule +{ + private ClassReflection $entityManagerClass; + + public function __construct( + private ReflectionProvider $reflectionProvider, + ) { + $this->entityManagerClass = $this->reflectionProvider->getClass(EntityManagerInterface::class); + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + + if ($node->name->toLowerString() !== 'createquery') { + return []; + } + + $calledOnType = $scope->getType($node->var); + + if ($this->isEntityManager($calledOnType)) { + return [ + RuleErrorBuilder::message('DQL is forbidden, use the query builder instead.') + ->identifier('afup.doctrine.noDQL') + ->build(), + ]; + } + + return []; + } + + private function isEntityManager(Type $type): bool + { + foreach ($type->getObjectClassNames() as $className) { + if ( + $className === $this->entityManagerClass->getName() + || $this->reflectionProvider->getClass($className)->isSubclassOfClass($this->entityManagerClass) + ) { + return true; + } + } + + return false; + } +} diff --git a/sources/AppBundle/StaticAnalysis/Rule/DoctrineRepositoryRule.php b/sources/AppBundle/StaticAnalysis/Rule/DoctrineRepositoryRule.php new file mode 100644 index 000000000..7554d0f14 --- /dev/null +++ b/sources/AppBundle/StaticAnalysis/Rule/DoctrineRepositoryRule.php @@ -0,0 +1,129 @@ + + */ +final readonly class DoctrineRepositoryRule implements Rule +{ + private const FORBIDDEN_METHODS = [ + 'createQueryBuilder', + 'createResultSetMappingBuilder', + 'findBy', + 'findOneBy', + 'getClassName', + 'matching', + 'count', + ]; + + private ClassReflection $repositoryClassReflection; + + public function __construct( + private ReflectionProvider $reflectionProvider, + ) { + $this->repositoryClassReflection = $this->reflectionProvider->getClass(EntityRepository::class); + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + + $methodName = $node->name->toString(); + + if ( + // Seules certaines méthodes sont interdites + !in_array($methodName, self::FORBIDDEN_METHODS, true) + + // On vérifie qu'on est bien dans une classe + || !$scope->isInClass() + + // Si l'appel est fait depuis l'intérieur d'un repository, c'est autorisé + || $this->isRepositoryClass($scope->getClassReflection()) + ) { + return []; + } + + $calledOnType = $scope->getType($node->var); + + // Si la méthode est déclarée dans une classe qui n'est pas un repository, c'est autorisé + if (!$this->isRepositoryClass($calledOnType)) { + return []; + } + + // Si la méthode est surchargée, c'est autorisé + if ($this->isMethodOverridden($calledOnType, $methodName)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf('Calling method %s() outside of repository classes is not allowed.', $methodName)) + ->identifier('afup.doctrine.repositoryMethods') + ->build(), + ]; + } + + private function isRepositoryClass(ClassReflection|Type $target): bool + { + if ($target instanceof Type) { + $classes = $target->getObjectClassNames(); + + foreach ($classes as $class) { + $classReflection = $this->reflectionProvider->getClass($class); + + if ($classReflection->getName() === $this->repositoryClassReflection->getName() + || $classReflection->isSubclassOfClass($this->repositoryClassReflection) + ) { + return true; + } + } + + return false; + } + + return $target->getName() === $this->repositoryClassReflection->getName() + || $target->isSubclassOfClass($this->repositoryClassReflection); + } + + private function isMethodOverridden(Type $type, string $methodName): bool + { + if ($type->isObject()->no()) { + return false; + } + + foreach ($type->getObjectClassNames() as $objectClassName) { + $classReflection = $this->reflectionProvider->getClass($objectClassName); + + if (!$classReflection->hasNativeMethod($methodName)) { + continue; + } + + $declaringClass = $classReflection->getNativeMethod($methodName)->getDeclaringClass(); + + if ($declaringClass->getName() === $this->repositoryClassReflection->getName()) { + return false; + } + } + + return true; + } +} diff --git a/tests/stubs/phpstan/Doctrine/DQLRepository.php b/tests/stubs/phpstan/Doctrine/DQLRepository.php new file mode 100644 index 000000000..7fa0267d0 --- /dev/null +++ b/tests/stubs/phpstan/Doctrine/DQLRepository.php @@ -0,0 +1,15 @@ +getEntityManager()->createQuery(''); + } +} diff --git a/tests/stubs/phpstan/Doctrine/NotRepository.php b/tests/stubs/phpstan/Doctrine/NotRepository.php new file mode 100644 index 000000000..62d816d95 --- /dev/null +++ b/tests/stubs/phpstan/Doctrine/NotRepository.php @@ -0,0 +1,27 @@ +createQueryBuilder(); + $this->createResultSetMappingBuilder(); + $this->findBy(); + $this->findOneBy(); + $this->getClassName(); + $this->matching(); + $this->count(); + } +} diff --git a/tests/stubs/phpstan/Doctrine/SomeExampleClass.php b/tests/stubs/phpstan/Doctrine/SomeExampleClass.php new file mode 100644 index 000000000..cfab40a93 --- /dev/null +++ b/tests/stubs/phpstan/Doctrine/SomeExampleClass.php @@ -0,0 +1,55 @@ +withoutOverridesRepository->createQueryBuilder('foo'); + $this->withoutOverridesRepository->createResultSetMappingBuilder('foo'); + $this->withoutOverridesRepository->findBy([]); + $this->withoutOverridesRepository->findOneBy([]); + $this->withoutOverridesRepository->getClassName(); + $this->withoutOverridesRepository->matching(Criteria::create()); + $this->withoutOverridesRepository->count(); + + $this->withAndWithoutOverridesRepository->createQueryBuilder('foo'); + $this->withAndWithoutOverridesRepository->createResultSetMappingBuilder('foo'); + $this->withAndWithoutOverridesRepository->findBy([]); + $this->withAndWithoutOverridesRepository->findOneBy([]); + $this->withAndWithoutOverridesRepository->getClassName(); + $this->withAndWithoutOverridesRepository->matching(Criteria::create()); + $this->withAndWithoutOverridesRepository->count(); + } + + public function allowedCalls(): void + { + $this->withOverridesRepository->createQueryBuilder('foo'); + $this->withOverridesRepository->createResultSetMappingBuilder('foo'); + $this->withOverridesRepository->findBy([]); + $this->withOverridesRepository->findOneBy([]); + $this->withOverridesRepository->getClassName(); + $this->withOverridesRepository->matching(Criteria::create()); + $this->withOverridesRepository->count(); + + $this->notRepository->createQueryBuilder(); + $this->notRepository->createResultSetMappingBuilder(); + $this->notRepository->findBy(); + $this->notRepository->findOneBy(); + $this->notRepository->getClassName(); + $this->notRepository->matching(); + $this->notRepository->count(); + } +} diff --git a/tests/stubs/phpstan/Doctrine/SubClassRepository.php b/tests/stubs/phpstan/Doctrine/SubClassRepository.php new file mode 100644 index 000000000..7bed537dc --- /dev/null +++ b/tests/stubs/phpstan/Doctrine/SubClassRepository.php @@ -0,0 +1,21 @@ +createQueryBuilder('foo'); + $this->createResultSetMappingBuilder('foo'); + $this->findBy([]); + $this->findOneBy([]); + $this->getClassName(); + $this->matching(Criteria::create()); + $this->count(); + } +} diff --git a/tests/stubs/phpstan/Doctrine/SubNotRepository.php b/tests/stubs/phpstan/Doctrine/SubNotRepository.php new file mode 100644 index 000000000..e649c7471 --- /dev/null +++ b/tests/stubs/phpstan/Doctrine/SubNotRepository.php @@ -0,0 +1,19 @@ +createQueryBuilder(); + $this->createResultSetMappingBuilder(); + $this->findBy(); + $this->findOneBy(); + $this->getClassName(); + $this->matching(); + $this->count(); + } +} diff --git a/tests/stubs/phpstan/Doctrine/WithOverridesRepository.php b/tests/stubs/phpstan/Doctrine/WithOverridesRepository.php new file mode 100644 index 000000000..73cce92c9 --- /dev/null +++ b/tests/stubs/phpstan/Doctrine/WithOverridesRepository.php @@ -0,0 +1,61 @@ +createQueryBuilder('foo'); + $this->createResultSetMappingBuilder('foo'); + $this->findBy([]); + $this->findOneBy([]); + $this->getClassName(); + $this->matching(Criteria::create()); + $this->count(); + } + + public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder + { + return parent::createQueryBuilder($alias, $indexBy); + } + + public function createResultSetMappingBuilder(string $alias): ResultSetMappingBuilder + { + return parent::createResultSetMappingBuilder($alias); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return parent::findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria, ?array $orderBy = null): ?object + { + return parent::findOneBy($criteria, $orderBy); + } + + public function getClassName(): string + { + return parent::getClassName(); + } + + public function matching(Criteria $criteria): AbstractLazyCollection&Selectable + { + return parent::matching($criteria); + } + + public function count(array $criteria = []): int + { + return parent::count($criteria); + } +} diff --git a/tests/stubs/phpstan/Doctrine/WithoutOverridesRepository.php b/tests/stubs/phpstan/Doctrine/WithoutOverridesRepository.php new file mode 100644 index 000000000..995b43596 --- /dev/null +++ b/tests/stubs/phpstan/Doctrine/WithoutOverridesRepository.php @@ -0,0 +1,22 @@ +createQueryBuilder('foo'); + $this->createResultSetMappingBuilder('foo'); + $this->findBy([]); + $this->findOneBy([]); + $this->getClassName(); + $this->matching(Criteria::create()); + $this->count(); + } +} diff --git a/tests/unit/AppBundle/StaticAnalysis/Rule/DoctrineDisableDQLRuleTest.php b/tests/unit/AppBundle/StaticAnalysis/Rule/DoctrineDisableDQLRuleTest.php new file mode 100644 index 000000000..0040d7fd2 --- /dev/null +++ b/tests/unit/AppBundle/StaticAnalysis/Rule/DoctrineDisableDQLRuleTest.php @@ -0,0 +1,24 @@ +createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/../../../../stubs/phpstan/Doctrine/DQLRepository.php'], [ + ['DQL is forbidden, use the query builder instead.', 13], + ]); + } +} diff --git a/tests/unit/AppBundle/StaticAnalysis/Rule/DoctrineRepositoryRuleTest.php b/tests/unit/AppBundle/StaticAnalysis/Rule/DoctrineRepositoryRuleTest.php new file mode 100644 index 000000000..8e87fb44b --- /dev/null +++ b/tests/unit/AppBundle/StaticAnalysis/Rule/DoctrineRepositoryRuleTest.php @@ -0,0 +1,44 @@ +createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/../../../../stubs/phpstan/Doctrine/SomeExampleClass.php'], [ + ['Calling method createQueryBuilder() outside of repository classes is not allowed.', 20], + ['Calling method createResultSetMappingBuilder() outside of repository classes is not allowed.', 21], + ['Calling method findBy() outside of repository classes is not allowed.', 22], + ['Calling method findOneBy() outside of repository classes is not allowed.', 23], + ['Calling method getClassName() outside of repository classes is not allowed.', 24], + ['Calling method matching() outside of repository classes is not allowed.', 25], + ['Calling method count() outside of repository classes is not allowed.', 26], + + ['Calling method createQueryBuilder() outside of repository classes is not allowed.', 28], + ['Calling method createResultSetMappingBuilder() outside of repository classes is not allowed.', 29], + ['Calling method findBy() outside of repository classes is not allowed.', 30], + ['Calling method findOneBy() outside of repository classes is not allowed.', 31], + ['Calling method getClassName() outside of repository classes is not allowed.', 32], + ['Calling method matching() outside of repository classes is not allowed.', 33], + ['Calling method count() outside of repository classes is not allowed.', 34], + ]); + + $this->analyse([__DIR__ . '/../../../../stubs/phpstan/Doctrine/WithoutOverridesRepository.php'], []); + $this->analyse([__DIR__ . '/../../../../stubs/phpstan/Doctrine/WithOverridesRepository.php'], []); + $this->analyse([__DIR__ . '/../../../../stubs/phpstan/Doctrine/SubClassRepository.php'], []); + $this->analyse([__DIR__ . '/../../../../stubs/phpstan/Doctrine/NotRepository.php'], []); + $this->analyse([__DIR__ . '/../../../../stubs/phpstan/Doctrine/SubNotRepository.php'], []); + } +}