diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile new file mode 100644 index 0000000000..01db59a35f --- /dev/null +++ b/.docker/php/Dockerfile @@ -0,0 +1,26 @@ +# syntax=docker/dockerfile:1 + +ARG PHP_VERSION=8.5-cli + +FROM composer:2 AS composer + +FROM php:$PHP_VERSION AS php + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +RUN apt-get update \ + && apt-get satisfy -qq --yes --no-install-recommends \ + "git (>= 1:2.30.2), git (<< 1:3), \ + libzip-dev (>= 1.7.3), libzip-dev (<< 2), \ + unzip (>= 6), unzip (<< 7), \ + zip (>= 3), zip (<< 4), \ + zlib1g-dev (>= 1:1.2.11.dfsg), zlib1g-dev (<< 1:2)" \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ + # Install PHP extensions + && docker-php-ext-install zip \ + && docker-php-ext-install pcntl \ + && docker-php-ext-install bcmath \ + && pecl install mongodb \ + && docker-php-ext-enable mongodb diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..ddf4f4e17b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,20 @@ +# Exclude nonessential files from dist +/.docker export-ignore +/.github export-ignore +/doc export-ignore +/example export-ignore +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php-cs-fixer.dist.php export-ignore +/.yamllint export-ignore +/CHANGELOG.md export-ignore +/CHANGELOG-v2.4.x.md export-ignore +/compose.yaml export-ignore +/CONTRIBUTING.md export-ignore +/Makefile export-ignore +/phpstan.neon.dist export-ignore +/phpstan-baseline.neon export-ignore +/phpunit.xml.dist export-ignore +/README.md export-ignore +/rector.php export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..56d2a367ec --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [l3pp4rd, stof, mbabker, phansys] diff --git a/.github/ISSUE_TEMPLATE/Bug.md b/.github/ISSUE_TEMPLATE/Bug.md new file mode 100644 index 0000000000..8b76cef3f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug.md @@ -0,0 +1,71 @@ +--- +name: ๐Ÿž Bug Report +about: Something is broken? ๐Ÿ”จ +labels: bug, unconfirmed +--- + + + + + +### Environment + +#### Package + +
show +

+ +``` +$ composer show --latest gedmo/doctrine-extensions +# Put the result here. +``` + +

+
+ +#### Doctrine packages + +
show +

+ +``` +$ composer show --latest 'doctrine/*' +# Put the result here. +``` + +

+
+ +#### PHP version + +``` +$ php -v +# Put the result here. +``` + +## Subject + + + +## Minimal repository with the bug + +## Steps to reproduce + +## Expected results + +## Actual results + + diff --git a/.github/ISSUE_TEMPLATE/Feature.md b/.github/ISSUE_TEMPLATE/Feature.md new file mode 100644 index 0000000000..5693087f68 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature.md @@ -0,0 +1,9 @@ +--- +name: ๐Ÿš€ Feature Request +about: I have a suggestion (and may want to implement it ๐Ÿ™‚)! +labels: feature +--- + +## Feature Request + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..a6a75d142a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: StackOverflow + url: https://stackoverflow.com/questions/tagged/doctrine-extensions + about: 'Questions tagged with "doctrine-extensions" on StackOverflow' + - name: Slack + url: https://symfony-devs.slack.com/archives/CCD2S9Y85 + about: '#doctrineextensions channel on Symfony Devs Slack' diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..b18fd29357 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 0000000000..edc003bf25 --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,112 @@ +name: "Coding Standards" + +on: + push: + branches: + - main + pull_request: + +jobs: + php-coding-standards: + name: "PHP-CS-Fixer" + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: "actions/checkout@v6" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.5" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "highest" + + - name: "Run PHP-CS-Fixer" + run: "vendor/bin/php-cs-fixer fix --ansi --verbose --diff --dry-run" + env: + PHP_CS_FIXER_IGNORE_ENV: 1 + + rector: + name: "Rector" + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: "actions/checkout@v6" + + - name: "Install PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: "8.5" + coverage: "none" + tools: "composer:v2" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "highest" + composer-options: "--prefer-dist --prefer-stable" + + - name: Rector + run: "vendor/bin/rector --no-progress-bar --dry-run" + + composer: + name: Composer + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: none + tools: composer:v2, composer-normalize:2 + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Lint Composer + run: make lint-composer + + lint-xml-files: + name: Lint XML files + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install required dependencies + run: sudo apt-get update && sudo apt-get install libxml2-utils + + - name: Lint XML files + run: make lint-xml + + lint-yaml-files: + name: Lint YAML files + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install Ruby 3.0 + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.0' + + - name: Install required gem + run: gem install yaml-lint + + - name: Lint YAML files + run: make lint-yaml diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000000..fbd9baa026 --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,148 @@ +name: "Continuous Integration" + +on: + push: + branches: + - main + pull_request: + +env: + MONGODB_SERVER: mongodb://127.0.0.1:27017 + +jobs: + phpunit: + name: "PHPUnit ${{ matrix.php-version }} (${{ matrix.deps }})${{ matrix.no-annotations == true && ' - Without Annotations' || '' }}${{ matrix.orm != '' && format(' - ORM {0}', matrix.orm) || '' }}" + runs-on: "ubuntu-latest" + + services: + mongo: + image: mongo + ports: + - 27017:27017 + + strategy: + fail-fast: false + matrix: + php-version: + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + - "8.5" + deps: + - "highest" + no-annotations: + - false + orm: + - "" + include: + - deps: "lowest" + php-version: "7.4" + - deps: "highest" + php-version: "8.5" + # Run builds on low and high PHP versions with `doctrine/annotations` removed + - deps: "highest" + php-version: "7.4" + no-annotations: true + - deps: "highest" + php-version: "8.5" + no-annotations: true + # Run builds on high PHP version with `doctrine/orm` version pinned + - deps: "highest" + php-version: "8.5" + orm: "^2.14" + - deps: "highest" + php-version: "8.5" + orm: "^3.0" + + steps: + - name: "Checkout" + uses: "actions/checkout@v6" + with: + fetch-depth: 2 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + extensions: mongodb + coverage: "pcov" + + # Remove PHP-CS-Fixer to avoid conflicting dependency ranges (i.e. doctrine/annotations) + - name: "Remove PHP-CS-Fixer" + run: "composer remove --dev --no-update friendsofphp/php-cs-fixer" + + # Remove doctrine/annotations if configured to do so + - name: "Remove doctrine/annotations" + if: "${{ matrix.no-annotations }}" + run: "composer remove --dev --no-update doctrine/annotations" + + # Pin doctrine/orm if configured to do so + - name: "Pin doctrine/orm" + if: "${{ matrix.orm }}" + run: "composer require --dev --no-update doctrine/orm:${{ matrix.orm }}" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "${{ matrix.deps }}" + + - name: "Run PHPUnit" + run: "vendor/bin/phpunit --coverage-clover coverage.xml" + + - name: "Upload coverage file" + uses: "actions/upload-artifact@v6" + with: + name: "${{ github.job }}-${{ matrix.php-version }}-${{ matrix.deps }}-${{ matrix.no-annotations == true && 'no-annotations' || 'with-annotations' }}${{ matrix.orm != '' && format('-orm-{0}', matrix.orm) || '' }}-coverage" + path: "coverage.xml" + + lint-doctrine-xml-schema: + name: Lint Doctrine XML schemas + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.2" + extensions: mongodb + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "highest" + + - name: Install required dependencies + run: sudo apt-get update && sudo apt-get install libxml2-utils + + - name: Lint xml files + run: make lint-doctrine-xml-schema + + upload_coverage: + name: "Upload coverage to Codecov" + runs-on: "ubuntu-latest" + needs: + - "phpunit" + + steps: + - name: "Checkout" + uses: "actions/checkout@v6" + with: + fetch-depth: 2 + + - name: "Download coverage files" + uses: "actions/download-artifact@v7" + with: + path: "reports" + + - name: "Upload to Codecov" + uses: "codecov/codecov-action@v5" + with: + directory: reports + token: "${{ secrets.CODECOV_TOKEN }}" diff --git a/.github/workflows/qa-dockerfile.yml b/.github/workflows/qa-dockerfile.yml new file mode 100644 index 0000000000..cb893a6bc4 --- /dev/null +++ b/.github/workflows/qa-dockerfile.yml @@ -0,0 +1,38 @@ +name: "Quality Assurance" + +on: + push: + branches: + - main + paths: + - ".docker/php/Dockerfile" + - "compose.yaml" + pull_request: + schedule: + - cron: "0 0 * * 0" + +jobs: + lint-dockerfile: + name: Hadolint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Lint Dockerfile + uses: hadolint/hadolint-action@v3.3.0 + with: + dockerfile: ".docker/php/Dockerfile" + + build: + runs-on: ubuntu-latest + name: Build containers with Docker Compose + steps: + - uses: actions/checkout@v6 + + - name: Build "php" container + uses: isbang/compose-action@v2.4.2 + with: + compose-file: "./compose.yaml" + services: | + php diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 0000000000..4bd27633d8 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,34 @@ +name: Quality Assurance + +on: + push: + branches: + - main + pull_request: + +jobs: + phpstan: + name: PHPStan + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: none + extensions: mongodb, zip + tools: composer:v2 + + - name: Install Composer dependencies (highest) + uses: ramsey/composer-install@v3 + with: + dependency-versions: highest + composer-options: --prefer-dist --prefer-stable --no-interaction --no-progress + + - name: PHPStan + run: vendor/bin/phpstan --memory-limit=1G analyse --error-format=github diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..25e3d6e8b8 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,28 @@ +name: Stale + +on: + schedule: + - cron: 0 9-18 * * * + +jobs: + stale: + runs-on: ubuntu-latest + + steps: + - name: Close stale issues and pull requests + uses: actions/stale@v10 + with: + days-before-close: 30 + days-before-stale: 180 + repo-token: ${{ secrets.GITHUB_TOKEN }} + exempt-issue-labels: 'Still Relevant' + stale-issue-label: 'Stale' + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + stale-pr-label: 'Stale' + stale-pr-message: > + This pull request has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. diff --git a/.gitignore b/.gitignore index 26ae9edcaf..aafb169765 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -tests/phpunit.xml -tests/temp/*.php -tests/temp/*.log -/vendor -/bin -/composer.lock -/composer.phar -.idea +.php-cs-fixer.cache +bin +vendor +composer.lock +coverage.xml +phpstan.neon +.phpunit.result.cache +phpunit.xml diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000000..a7481fdb02 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,96 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +$header = <<<'HEADER' + This file is part of the Doctrine Behavioral Extensions package. + (c) Gediminas Morkevicius http://www.gediminasm.org + For the full copyright and license information, please view the LICENSE + file that was distributed with this source code. + HEADER; + +$finder = PhpCsFixer\Finder::create() + ->in([ + __DIR__.'/example', + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->append([__FILE__, __DIR__.'/rector.php']) + ->exclude([ + __DIR__.'/tests/data', + ]); + +return (new PhpCsFixer\Config()) + ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) + ->setRules([ + '@DoctrineAnnotation' => true, + '@PHP7x4Migration' => true, + '@PHP7x4Migration:risky' => true, + '@PHPUnit9x1Migration:risky' => true, + '@PSR2' => true, + '@Symfony' => true, + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_before_statement' => true, + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + // @todo: Change the following rule to `true` in the next major release. + 'declare_strict_types' => false, + 'error_suppression' => true, + 'global_namespace_import' => ['import_classes' => false, 'import_constants' => false, 'import_functions' => false], + 'header_comment' => ['header' => $header], + 'is_null' => true, + 'list_syntax' => ['syntax' => 'short'], + 'modernize_types_casting' => true, + 'no_homoglyph_names' => true, + 'no_null_property_initialization' => true, + 'no_superfluous_elseif' => true, + 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], + 'no_unset_on_property' => true, + 'no_useless_else' => true, + 'nullable_type_declaration_for_default_null_value' => true, + 'ordered_class_elements' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'phpdoc_order' => ['order' => ['param', 'throws', 'return']], + 'phpdoc_separation' => ['groups' => [ + ['Gedmo\\*'], + ['ODM\\*'], + ['ORM\\*'], + ]], + 'phpdoc_summary' => false, + 'phpdoc_to_comment' => false, + 'php_unit_construct' => true, + 'php_unit_dedicate_assert' => true, + 'php_unit_dedicate_assert_internal_type' => true, + 'php_unit_mock' => true, + 'php_unit_namespaced' => true, + 'php_unit_set_up_tear_down_visibility' => true, + 'php_unit_strict' => true, + 'php_unit_test_annotation' => ['style' => 'prefix'], + 'php_unit_test_case_static_method_calls' => true, + 'psr_autoloading' => true, + 'random_api_migration' => true, + 'return_assignment' => true, + 'self_accessor' => true, + 'static_lambda' => true, + 'strict_param' => true, + // @todo: Change the following rule to `true` when support for PHP < 8 is dropped. + 'stringable_for_to_string' => false, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline' => [ + 'elements' => [ + 'arrays', + ], + ], + // @todo: Change the following rule to `true` in the next major release. + 'void_return' => false, + ]) + ->setFinder($finder) + ->setRiskyAllowed(true) + ->setUsingCache(true); diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 08be3a6551..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -language: php - -sudo: false - -php: - - 5.4 - - 5.5 - - 5.6 - - 7.0 - -matrix: - allow_failures: - - php: 7.0 - -services: mongodb - -before_install: - - if [[ "$TRAVIS_PHP_VERSION" = 5.* ]]; then echo 'extension=mongo.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi - - if [[ "$TRAVIS_PHP_VERSION" != 5.* ]]; then composer remove doctrine/mongodb-odm --no-update --dev; fi - -install: - - composer install --prefer-dist - -script: - - bin/phpunit -c tests/ - -notifications: - email: - - gediminas.morkevicius@gmail.com - - developers@atlantic18.com diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000000..ac09cc8c09 --- /dev/null +++ b/.yamllint @@ -0,0 +1,16 @@ +ignore: vendor/ + +extends: default + +rules: + comments: disable + comments-indentation: disable + document-start: disable + empty-lines: + max: 1 + max-start: 0 + max-end: 0 + line-length: disable + truthy: + allowed-values: ['true', 'false'] + check-keys: false diff --git a/CHANGELOG-v2.4.x.md b/CHANGELOG-v2.4.x.md new file mode 100644 index 0000000000..4f076e82f5 --- /dev/null +++ b/CHANGELOG-v2.4.x.md @@ -0,0 +1,57 @@ +# Doctrine Extensions Changelog - v2.4.x + +:warning: This is an archived changelog from the v2.4.x history of Doctrine Extensions. +View the main [CHANGELOG.md](CHANGELOG.md) file for the most recent version history. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +--- + +## [2.4.42] - 2020-08-20 +### Translatable +#### Fixed +- Allow for both falsy and null-fallback translatable values (#2152) + +## [2.4.41] - 2020-05-10 +### Sluggable +#### Fixed +- Remove PHPDoc samples as they are interpreted by Annotation Reader (#2120) + +## [2.4.40] - 2020-04-27 +### SoftDeleteable +#### Fixed +- Invalidate query cache when toggling filter on/off for an entity (#2112) + +## [2.4.39] - 2020-01-18 +### Tree +#### Fixed +- The value of path source property is cast to string type for Materialized Path Tree strategy (#2061) + +## [2.4.38] - 2019-11-08 +### Global / Shared +#### Fixed +- Add `parent::__construct()` calls to Listeners w/ custom constructors (#2012) +- Add upcoming Doctrine ODM 2.0 to `composer.json` conflicts (#2027) + +### Loggable +#### Fixed +- Added missing string casting of `objectId` in `LogEntryRepository::revert()` method (#2009) + +### ReferenceIntegrity +#### Fixed +- Get class from meta in ReferenceIntegrityListener (#2021) + +### Translatable +#### Fixed +- Return default AST executor instead of throwing Exception in Walker (#2018) +- Fix duplicate inherited properties (#2029) + +### Tree +#### Fixed +- Remove hard-coded parent column name in repository prev/next sibling queries (#2020) + +## [2.4.37] - 2019-03-17 +### Translatable +#### Fixed +- Bugfix to load null value translations (#1990) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..735c5de2b5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,401 @@ +# Doctrine Extensions Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +Each release should include sub-headers for the Extension above the types of +changes, in order to more easily recognize how an Extension has changed in +a release. + +```markdown +## [3.6.1] - 2022-07-26 +### Fixed +- Sortable: Fix issue with add+delete position synchronization (#1932) +``` + +--- + +## [Unreleased] +### Changed +- All: Removed the dollar sign from the generated cache ID for extension metadata to ensure only characters mandated by [PSR-6](https://www.php-fig.org/psr/psr-6/#definitions) are used, improving compatibility with caching implementations with strict character requirements (#2978) + +## [3.22.0] - 2025-12-13 +### Added +- Support for Symfony 8 + +## [3.21.0] - 2025-09-22 +### Added +- SoftDeleteable: `$handlePostFlushEvent` parameter to `SoftDeleteableListener::__construct()` to enable or disable handling of the `postFlush` event (#2958) + +### Changed +- Translatable: Optimized database indexes for better performance by reordering unique constraint fields and removing redundant indexes +- SoftDeleteable: Handling of the `postFlush` event is disabled by default (#2958) +- Sluggable: Replaced abandoned `behat/transliterator` with `symfony/string` for default transliteration and urlization steps (#2985) +- Use `ClassMetadata::getFieldValue()` and `ClassMetadata::setFieldValue()` methods to support `doctrine/orm` >= 3.4 (#2966) + +### Fixed +- SoftDeleteable: Prevent cascade persist from re-inserting soft-deleted entities still referenced in the identity map (#2958) +- Sluggable: Fix type error when generating slug using embedded properties (#2965) + +## [3.20.1] - 2025-09-14 +### Fixed +- Compatibility with `doctrine/mongodb-odm` ^2.11 (#2945) + +## [3.20.0] - 2025-04-04 +### Fixed +- SoftDeleteable: Resolved a bug where a soft-deleted object isn't remove from the ObjectManager (#2930) + +### Added +- IP address provider for use with extensions with IP address references (#2928) + +## [3.19.0] - 2025-02-24 +### Added +- Actor provider for use with extensions with user references (#2914) + +### Changed +- Updated minimum versions for `doctrine/orm` to ^2.20 || ^3.3 + +### Fixed +- Regression with `doctrine/orm` ^2.20 || ^3.3 that caused the translation walker to produce queries with duplicated LIMIT clauses (issue #2917) + +## [3.18.0] - 2025-02-01 +### Added +- Support for `doctrine/persistence` ^4.0 + +### Deprecated +- Sluggable: Annotation-specific mapping parameters (#2837) + +### Fixed +- Fix regression with `doctrine/dbal` >= 4.0 that caused MariaDB to improperly attempt LONGTEXT casting in `TranslationWalker` (issue #2887) +- Tree: allow usage of UuidV4 as path source with the materialized path strategy + +## [3.17.1] - 2024-10-07 +### Fixed +- Removed invalid `@note` annotation from `AbstractLogEntry::$data`, which was causing issues in projects using annotation parsers + +## [3.17.0] - 2024-10-06 +### Added +- Support for `doctrine/dbal` >= 4.0 with all extensions, except Loggable + +### Changed +- Extend `Throwable` from `Gedmo\Exception` interface + +## [3.16.1] - 2024-06-25 +### Fixed +- Restructure the SqlWalkerCompat trait to fix optimized autoloading + +## [3.16.0] - 2024-06-24 +### Added +- Support for `doctrine/orm` 3 +- Blameable: Added UUID in allowed types list for Blameable fields in Annotation +- Blameable: Allow ascii_string to validTypes (issue #2726) +- Sluggable: Allow ascii_string to validTypes +- IpTraceable: Allow ascii_string to validTypes +- Sluggable: Use `TranslationWalker` hint when looking for similar slugs (`getSimilarSlugs` method) for entities which implement `Translatable` interface and have `uniqueOverTranslations: true` Slug option (#100, #2530) + +### Deprecated +- Support for defining mapping information from annotations has been deprecated and will be removed in 4.0, use PHP attributes mapping instead. + +### Fixed +- Tree: Cascade remove not being triggered on entity children at `MaterializedPath::removeNode()`. +- Tree: Materialize Path strategy when using autogenerated identifiers. + +## [3.15.0] - 2024-02-12 +### Added +- SoftDeleteable: `Gedmo\SoftDeleteable\Event\PreSoftDeleteEventArgs` and + `Gedmo\SoftDeleteable\Event\PostSoftDeleteEventArgs` classes. +- Add support for injecting a `psr/clock` implementation into event adapters + that create new `DateTimeInterface` objects (SoftDeleteable and Timestampable) + +### Changed +- Make doctrine/annotations an optional dependency. +- Remove `@internal` annotation from `Gedmo\Mapping\Driver\AttributeReader`. + +### Deprecated +- Do not add type-hinted parameters `Gedmo\SoftDeleteable\Event\PreSoftDeleteEventArgs` and + `Gedmo\SoftDeleteable\Event\PostSoftDeleteEventArgs` classes to `preSoftDelete` and `postSoftDelete` events. +- The `createLifecycleEventArgsInstance()` method on `Gedmo\Mapping\Event\AdapterInterface` + implementations is deprecated, use your own subclass of `Doctrine\Persistence\Event\LifecycleEventArgs` as needed. + +### Fixed +- Add conflict against "doctrine/orm" >= 3. +- Add conflict against "doctrine/dbal" => 4. + +## [3.14.0] - 2023-12-03 +### Added +- Support for Symfony 7 +- Tree: Added `@template` and `@template-extends` annotations to the Tree repositories + +### Changed +- Dropped support for PHP < 7.4 +- Dropped support for Symfony < 5.4 +- Dropped support for doctrine/dbal < 3.2 + +### Deprecated +- Calling `Gedmo\Mapping\Event\Adapter\ORM::getObjectManager()` and `getObject()` on EventArgs that do not implement `getObjectManager()` and `getObject()` (such as old EventArgs implementing `getEntityManager()` and `getEntity()`) +- Calling `Gedmo\Uploadable\Event\UploadableBaseEventArgs::getEntityManager()` and `getEntity()`. Call `getObjectManager()` and `getObject()` instead. + +## [3.13.0] - 2023-09-06 +### Fixed +- References: fixed condition in `XML` Driver that did not allow to retrieve from the entity definition the `mappedBy` and `inversedBy` fields. +- Fix bug collecting metadata for inherited mapped classes + +## [3.12.0] - 2023-07-08 +### Added +- Tree: `setSibling()` and `getSibling()` methods in the `Node` interface through the BC `@method` annotation +- Tree: Support array of fields and directions in the `$sortByField` and `$direction` parameters at `AbstractTreeRepository::recover()` +- Loggable: Support for composite identifiers + +### Changed +- Named arguments have precedence over the values passed in the `$data` array in annotation classes at `Gedmo\Mapping\Annotation\` + namespace +- Removed conflict against "doctrine/cache" < 1.11, as this library is not used +- Return type from `TranslationProxy::__set()` (from `TranslationProxy` to `void`) + +### Fixed +- Tree: Creation of dynamic `Node::$sibling` property, which is deprecated as of PHP >= 8.2 +- Return type from `TranslationProxy::__set()` in order to honor its original signature (`void`) + +### Deprecated +- Tree: Not implementing `Node` interface in classes that are used as nodes +- Implementing the `Gedmo\Tool\WrapperInterface::getIdentifier()` method without the second argument (`$flatten`) is deprecated and will + be required in version 4.0 + +## [3.11.1] - 2023-02-20 +### Fixed +- Loggable: Remove unfixable deprecation when extending `LoggableListener` +- Remove unfixable deprecations when extending repository classes +- Fix error caused by the attempt of "doctrine/annotations" parsing a `@note` annotation + +## [3.11.0] - 2023-01-26 +### Added +- Tree: [NestedSet] Added "base" property for tree level annotation +- Tree: [NestedSet] Added `$options` as parameter 2 in `getPathQueryBuilder()` to specify whether you want the starting node included or not +- Tree: [NestedSet] Added `getPathAsString()` method to entity repository +- Tree: [NestedSet] Added "treeRootNode" option in `verify()` in case you want to verify a single tree in a forest +- Tree: [NestedSet] Added `recoverFast()` method for where speed is more important than safety and entity manager state +- Tree: [NestedSet] Added options to `recover()` for sibling order, tree root in a forest, verification skip and auto-flushing +- Tree: [NestedSet] Verify and recover wrong levels in nested set + +### Added +- Tree: Add `Nested::ALLOWED_NODE_POSITIONS` constant in order to expose the available node positions +- Support for `doctrine/collections` 2.0 +- Support for `doctrine/event-manager` 2.0 +- Loggable: Add `LogEntryInterface` interface in order to be implemented by log entry models + +### Fixed +- Sortable: Fix return value check of Comparable interface (#2541) +- Uploadable: Retrieve the correct metadata when uploading entities of different classes (#2071) +- Translatable: Fix property existence check at `TranslatableListener::getTranslatableLocale()` + +### Deprecated +- In order to close the API, `@final` and `@internal` annotations were added to all non base classes, which means that extending + these classes is deprecated and can not be inherited in version 4.0. +- Sortable: Accepting a return type other than "integer" from `Comparable::compareTo()` is deprecated in `SortableListener::postFlush()`. + This will not be accepted in version 4.0. +- Deprecate the annotation reader being allowed to be any object. + In 4.0, a `Doctrine\Common\Annotations\Reader` or `Gedmo\Mapping\Driver\AttributeReader` instance will be required. +- `Gedmo\DoctrineExtensions::registerAnnotations()` is deprecated and will be removed in 4.0, the method has been no-op'd as all + supported `doctrine/annotations` versions support autoloading +- Loggable: Constants `LoggableListener::ACTION_CREATE`, `LoggableListener::ACTION_UPDATE` and `LoggableListener::ACTION_REMOVE` + are deprecated. Use `LogEntryInterface::ACTION_CREATE`, `LogEntryInterface::ACTION_UPDATE` and `LogEntryInterface::ACTION_REMOVE` + instead. + +## [3.10.0] - 2022-11-14 +### Changed +- Bump "doctrine/event-manager" dependency from ^1.0 to ^1.2. + +### Fixed +- Tree: TreeRoot without rootIdentifierMethod when calling getNextSiblings (#2518) +- Sortable: Fix duplicated positions when manually updating position on more than one object (#2439) + +## [3.9.0] - 2022-09-22 +### Fixed +- Tree: Allow sorting children by a ManyToOne relation (#2492) +- Tree: Fix passing `null` to `abs()` function +- Timestampable: Use an attribute in Timestampable attribute docs + +### Deprecated +- Tree: Passing `null` as argument 8 to `Nested::shiftRangeRL()` + +## [3.8.0] - 2022-07-17 +### Added +- Sluggable: Add support for `DateTimeImmutable` fields +- Tree: [NestedSet] `childrenQueryBuilder()` to allow specifying sort order separately for each field +- Tree: [NestedSet] Added option to reorder only direct children in `reorder()` method + +### Changed +- Tree: In `ClosureTreeRepository::removeFromTree()` and `NestedTreeRepository::removeFromTree()` when something fails in the transaction, it uses the `code` from the original exception to construct the `\Gedmo\Exception\RuntimeException` instance instead of `null`. + +### Fixed +- Sluggable: Cast slug to string before passing it as argument 2 to `preg_match()` (#2473) +- Sortable: [SortableGroup] Fix sorting date columns in SQLite (#2462). +- PHPDoc of `AbstractMaterializedPath::removeNode()` and `AbstractMaterializedPath::getChildren()` +- Retrieving the proper metadata cache from Doctrine when using a CacheWarmer. + +## [3.7.0] - 2022-05-17 +### Added +- Add support for doctrine/persistence 3 + +### Changed +- Removed call to deprecated `ClassMetadataFactory::getCacheDriver()` method. +- Dropped support for doctrine/mongodb-odm < 2.3. +- Make doctrine/cache an optional dependency. + +### Fixed +- Loggable: Fix `appendNumber` renaming for files without extension (#2228) + +## [3.6.0] - 2022-03-19 +### Added +- Translatable: Add defaultTranslationValue option to allow null or string value (#2167). TranslatableListener can hydrate object properties with null value, but it may cause a Type error for non-nullable getter upon a missing translation. + +### Fixed +- Uploadable: `FileInfoInterface::getSize()` return type declaration (#2413). +- Tree: Setting a new Tree Root when Tree Parent is `null`. +- Tree: update cache key used by Closure to match Doctrine's one (#2416). +- Tree: persist order does not affect entities on Closure (#2432) + +## [3.5.0] - 2022-01-10 +### Added +- SoftDeleteable: Support to use annotations as attributes on PHP >= 8.0. +- Blameable: Support to use annotations as attributes on PHP >= 8.0. +- IpTraceable: Support to use annotations as attributes on PHP >= 8.0. +- Sortable: Support to use annotations as attributes on PHP >= 8.0. +- Sluggable: Support to use annotations as attributes on PHP >= 8.0. +- Uploadable: Support to use annotations as attributes on PHP >= 8.0. +- Tree: Support to use annotations as attributes on PHP >= 8.0. +- References: Support to use annotations as attributes on PHP >= 8.0. +- ReferenceIntegrity: Support to use annotations as attributes on PHP >= 8.0. +- SoftDeleteable: Support for custom column types (like Carbon). +- Timestampable: Support for custom column types (like Carbon). +- Translatable: Added an index to `Translation` entity to speed up searches using + `Gedmo\Translatable\Entity\Repository\TranslationRepository::findTranslations()` method. +- `Gedmo\Mapping\Event\AdapterInterface::getObject()` method. + +### Fixed +- Blameable, IpTraceable, Timestampable: Type handling for the tracked field values configured in the origin field. +- Loggable: Using only PHP 8 attributes. +- References: Avoid deprecations using LazyCollection with PHP 8.1 +- Tree: Association mapping problems using Closure tree strategy (by manually defining mapping on the closure entity). +- Wrong PHPDoc type declarations. +- Avoid calling deprecated `AbstractClassMetadataFactory::getCacheDriver()` method. +- Avoid deprecations using `doctrine/mongodb-odm` >= 2.2 +- Translatable: `Gedmo\Translatable\Document\Repository\TranslationRepository::findObjectByTranslatedField()` + method accessing a non-existing key. + +### Deprecated +- Tree: When using Closure tree strategy, it is deprecated not defining the mapping associations of the closure entity. +- `Gedmo\Tool\Logging\DBAL\QueryAnalizer` class without replacement. +- Using YAML mapping is deprecated, you SHOULD migrate to attributes, annotations or XML. +- `Gedmo\Mapping\Event\AdapterInterface::__call()` method. +- `Gedmo\Tool\Wrapper\AbstractWrapper::clear()` method. +- `Gedmo\Tool\Wrapper\WrapperInterface::populate()` method. + +### Changed +- In order to use a custom cache for storing configuration of an extension, the user has to call `setCacheItemPool()` + on the extension listener passing an instance of `Psr\Cache\CacheItemPoolInterface`. + +## [3.4.0] - 2021-12-05 +### Added +- PHP 8 Attributes support for Doctrine MongoDB to document & traits. +- Support for doctrine/dbal >=3.2. +- Timestampable: Support to use annotations as attributes on PHP >= 8.0. +- Loggable: Support to use annotations as attributes on PHP >= 8.0. + +### Changed +- Translatable: Dropped support for other values than "true", "false", "1" and "0" in the `fallback` attribute of the `translatable` + element in the XML mapping. +- Tree: Dropped support for other values than "true", "false", "1" and "0" in the `activate-locking` attribute of the `tree` + element in the XML mapping. +- Tree: Dropped support for other values than "true", "false", "1" and "0" in the `append_id`, `starts_with_separator` and + `ends_with_separator` attributes of the `tree-path` element in the XML mapping. +- Dropped support for doctrine/dbal < 2.13.1. +- The third argument of `Gedmo\SoftDeleteable\Query\TreeWalker\Exec\MultiTableDeleteExecutor::__construct()` requires a `Doctrine\ORM\Mapping\ClassMetadata` instance. + +## [3.3.1] - 2021-11-18 +### Fixed +- Translatable: Using ORM/ODM attribute mapping and translatable annotations. +- Tree: Missing support for `tree-path-hash` fields in XML mapping. +- Tree: Check for affected rows at `ClosureTreeRepository::cleanUpClosure()` and `Closure::updateNode()`. +- `Gedmo\Mapping\Driver\Xml::_loadMappingFile()` behavior in scenarios where `libxml_disable_entity_loader(true)` was previously + called. +- Loggable: Missing support for `versioned` fields at `attribute-override` in XML mapping. + +## [3.3.0] - 2021-11-15 +### Added +- Support to use Translatable annotations as attributes on PHP >= 8.0. + +### Deprecated +- `Gedmo\Mapping\Driver\File::$_paths` property and `Gedmo\Mapping\Driver\File::setPaths()` method are deprecated and will + be removed in version 4.0, as they are not used. + +### Fixed +- Value passed in the `--config` option to `fix-cs` Composer script. +- Return value for `replaceRelative()` and `replaceInverseRelative()` at `Gedmo\Sluggable\Mapping\Event\Adapter\ODM` if the + query result does not implement `Doctrine\ODM\MongoDB\Iterator\Iterator`. +- Restored compatibility with doctrine/orm >= 2.10.2 (#2272). + Since doctrine/orm 2.10, `Doctrine\ORM\UnitOfWork` relies on SPL object IDs instead of hashes, thus we need to adapt our codebase in order to be compatible with this change. + As `Doctrine\ODM\MongoDB\UnitOfWork` from doctrine/mongodb-odm still uses `spl_object_hash()`, all `spl_object_hash()` calls were replaced by `spl_object_id()` to make it work with both ORM and ODM managers. + +## [3.2.0] - 2021-10-05 +### Added +- PHP 8 Attributes for Doctrine ORM to entities & traits (#2251) + +### Fixed +- Removed legacy checks targeting older versions of PHP (#2201) +- Added missing XSD definitions (#2244) +- Replaced undefined constants from `Doctrine\DBAL\Types\Type` at `Gedmo\Translatable\Mapping\Event\Adapter\ORM::foreignKey()` (#2250) +- Add conflict against "doctrine/orm" >=2.10 in order to guarantee the schema extension (see https://github.com/doctrine/orm/pull/8852) (#2255) + +## [3.1.0] - 2021-06-22 +### Fixed +- Allow installing doctrine/cache 2.0 (thanks @alcaeus!) +- Make doctrine/cache a dev dependency + +## [3.0.5] - 2021-04-23 +### Fixed +- Use path_separator when removing children (#2217) + +## [3.0.4] - 2021-03-27 +### Fixed +- Add hacky measure to resolve incompatibility with DoctrineBundle 2.3 [#2211](https://github.com/doctrine-extensions/DoctrineExtensions/pull/2211) + +## [3.0.3] - 2021-01-23 +### Fixed +- Add PHP 8 compatibility to `composer.json`, resolving minor function parameter deprecations [#2194](https://github.com/Atlantic18/DoctrineExtensions/pull/2194) + +## [3.0.2] - 2021-01-23 +- Ignore; tag & version mismatch + +## [3.0.1] - 2021-01-23 +- Ignore; wrong branch published + +## [3.0.0] - 2020-09-23 +### Notable & Breaking Changes +- Minimum PHP version requirement of 7.2 +- Source files moved from `/lib/Gedmo` to `/src` +- Added compatibility for `doctrine/common` 3.0 and `doctrine/persistence` 2.0 +- All string column type annotations changed to 191 character length (#1941) +- Removed support for `\Zend_date` date format [#2163](https://github.com/Atlantic18/DoctrineExtensions/pull/2163) +- Renamed `Zend Framework` to `Laminas` [#2163](https://github.com/Atlantic18/DoctrineExtensions/pull/2163) + +Changes below marked โš ๏ธ may also be breaking, if you have extended DoctrineExtensions. + +### MongoDB +- Requires the `ext-mongodb` PHP extension. Usage of `ext-mongo` is deprecated and will be removed in the next major version. +- Minimum Doctrine MongoDB ODM requirement of 2.0 +- Usages of `\MongoDate` replaced with `MongoDB\BSON\UTCDateTime` + +### Global / Shared +#### Fixed +- Removed `null` parameter from `Doctrine\Common\Cache\Cache::save()` calls (#1996) + +### Tree +#### Fixed +- The value of path source property is cast to string type for Materialized Path Tree strategy (#2061) + +### SoftDeleteable +#### Changed +- โš ๏ธ Generate different Date values based on column type (#2115) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..11cddd7747 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing to Doctrine Extensions + +Thank you for your interest in contributing to Doctrine Extensions! + +## Which Branch Should I Contribute To? + +All pull requests (new features and bug fixes) should target the `main` branch. +Anything that can be back-ported to v2.4.x will be done by maintainers. + +:warning: The `v.2.4.x` branch has been marked as legacy/deprecated. + +## Pull Request Titles + +Please include the name(s) of the related extensions as a "tag" in the +pull request title. + +> [Tree] Add a new Oak Tree branching style + +## Changelog + +All updates must include an entry in the [Changelog](/CHANGELOG.md). +Put your entry in the `[Unreleased]` section at the top, under the +corresponding Extension and Category. + +If there is a related GitHub issue, add it as a suffix to your change. + +``` +## [Unreleased] +### Fixed +- Loggable: Allow emoji in the docs (#123) +``` + +## What You Can Contribute + +Want to contribute but aren't sure where to start? Check out our +[Issue Board](https://github.com/Atlantic18/DoctrineExtensions/issues)! +There are lots of opportunities for helping other users with their issue, +or contributing a reported bug fix or feature request. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..46549f9afb --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +all: + @echo "Please choose a task." +.PHONY: all + +lint: lint-composer lint-yaml lint-xml +.PHONY: lint + +lint-composer: + composer-normalize --dry-run + composer validate +.PHONY: lint-composer + +lint-xml: + find './tests/.' \( -name '*.xml' \) \ + | while read xmlFile; \ + do \ + XMLLINT_INDENT=' ' xmllint --encode UTF-8 --format "$$xmlFile"|diff - "$$xmlFile"; \ + if [ $$? -ne 0 ]; then echo "$$xmlFile" && exit 1; fi; \ + done + +.PHONY: lint-xml + +lint-doctrine-xml-schema: + find './tests/Gedmo/Mapping/Driver/Xml/.' \( -name '*.xml' \) \ + | while read xmlFile; \ + do \ + xmllint --encode UTF-8 --format "$$xmlFile" --schema "./doctrine-mapping.xsd"; \ + if [ $$? -ne 0 ]; then echo "$$xmlFile" && exit 1; fi; \ + done + +.PHONY: lint-doctrine-xml-schema + +cs-fix-doctrine-xml: + find './tests/Gedmo/Mapping/Driver/Xml/.' \( -name '*.xml' \) \ + | while read xmlFile; \ + do \ + XMLLINT_INDENT=' ' xmllint --encode UTF-8 --format "$$xmlFile" --output "$$xmlFile"; \ + done +.PHONY: cs-fix-doctrine-xml + +lint-yaml: + yamllint . + +.PHONY: lint-yaml diff --git a/README.md b/README.md index 5972543b0b..21b21eac4f 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,83 @@ -# Doctrine2 behavioral extensions +# Doctrine Behavioral Extensions -**Version 2.4.9** +[![Continuous Integration](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/continuous-integration.yml) +[![Quality Assurance](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/qa.yml/badge.svg)](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/qa.yml) +[![Coding Standards](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/coding-standards.yml/badge.svg)](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/coding-standards.yml) +[![Latest Stable Version](https://poser.pugx.org/gedmo/doctrine-extensions/version)](https://packagist.org/packages/gedmo/doctrine-extensions) -[![Build Status](https://secure.travis-ci.org/Atlantic18/DoctrineExtensions.png?branch=master)](http://travis-ci.org/Atlantic18/DoctrineExtensions) +This package contains extensions for Doctrine ORM and MongoDB ODM that offer new functionality or tools to use Doctrine +more efficiently. These behaviors can be easily attached to the event system of Doctrine and handle the records being +flushed in a behavioral way. -**Note:** Extensions **2.4.x** are compatible with ORM and doctrine common library versions from **2.2.x** to **2.5.x**. -ORM 2.5.x versions require **PHP 5.4** or higher. +--- -**Note:** Extensions **2.3.x** are compatible with ORM and doctrine common library versions from **2.2.x** to **2.4.x** -**Note:** If you are setting up entity manager without a framework, see the [example](/example/em.php) to prevent issues like #1310 +## Doctrine Extensions 3.0 Released :tada: -### Latest updates +3.0 focuses on refreshing this package for today's PHP. This includes: -**2015-05-01** +- Bumping minimum version requirements of PHP, Doctrine, and other dependencies +- Implementing support for the latest Doctrine MongoDB & Common packages +- Updating the test suite, add code and style standards, and other needed build tools +- Cleaning up documentation, code, comments, etc. -- Reverted back [1272](https://github.com/Atlantic18/DoctrineExtensions/pull/1272) and see [1263](https://github.com/Atlantic18/DoctrineExtensions/issues/1263). Use [naming strategy](http://stackoverflow.com/questions/12702657/how-to-configure-naming-strategy-in-doctrine-2) for your use cases. -- Fixed bug for sortable [1279](https://github.com/Atlantic18/DoctrineExtensions/pull/1279) +[Read the Upgrade Doc for more info.](/doc/upgrading/upgrade-v2.4-to-v3.0.md) -**2015-03-26** +--- -Support for ORM and Common library **2.5.0**. A minor version bump, because of trait column changes. +## Installation -**2015-01-28** + composer require gedmo/doctrine-extensions -Fixed the issue for all mappings, which caused related class mapping failures, when a relation or class name -was in the same namespace, but extensions required it to be mapped as full classname. +* [Symfony](/doc/frameworks/symfony.md) +* [Laravel](/doc/frameworks/laravel.md) +* [Laminas](/doc/frameworks/laminas.md) -**2015-01-21** +### Upgrading -Fixed memory leak issue with entity or document wrappers for convenient metadata retrieval. +* [From 2.4.x to 3.0](/doc/upgrading/upgrade-v2.4-to-v3.0.md) -### Summary and features +## Extensions -This package contains extensions for Doctrine2 that hook into the facilities of Doctrine and -offer new functionality or tools to use Doctrine2 more efficiently. This package contains mostly -used behaviors which can be easily attached to your event system of Doctrine2 and handle the -records being flushed in the behavioral way. List of extensions: +#### ORM & MongoDB ODM -- [**Tree**](/doc/tree.md) - this extension automates the tree handling process and adds some tree specific functions on repository. -(**closure**, **nestedset** or **materialized path**) -- [**Translatable**](/doc/translatable.md) - gives you a very handy solution for translating records into different languages. Easy to setup, easier to use. -- [**Sluggable**](/doc/sluggable.md) - urlizes your specified fields into single unique slug -- [**Timestampable**](/doc/timestampable.md) - updates date fields on create, update and even property change. - [**Blameable**](/doc/blameable.md) - updates string or reference fields on create, update and even property change with a string or object (e.g. user). - [**Loggable**](/doc/loggable.md) - helps tracking changes and history of objects, also supports version management. +- [**Sluggable**](/doc/sluggable.md) - urlizes your specified fields into single unique slug +- [**Timestampable**](/doc/timestampable.md) - updates date fields on create, update and even property change. +- [**Translatable**](/doc/translatable.md) - gives you a very handy solution for translating records into different languages. Easy to setup, easier to use. +- [**Tree**](/doc/tree.md) - automates the tree handling process and adds some tree-specific functions on repository. +(**closure**, **nested set** or **materialized path**) + _(MongoDB ODM only supports materialized path)_ + +#### ORM Only + +- [**IpTraceable**](/doc/ip_traceable.md) - inherited from Timestampable, sets IP address instead of timestamp +- [**SoftDeleteable**](/doc/soft-deleteable.md) - allows to implicitly remove records - [**Sortable**](/doc/sortable.md) - makes any document or entity sortable -- [**Translator**](/doc/translatable.md) - explicit way to handle translations -- [**SoftDeleteable**](/doc/softdeleteable.md) - allows to implicitly remove records - [**Uploadable**](/doc/uploadable.md) - provides file upload handling in entity fields + +#### MongoDB ODM Only + - [**References**](/doc/references.md) - supports linking Entities in Documents and vice versa - [**ReferenceIntegrity**](/doc/reference_integrity.md) - constrains ODM MongoDB Document references -- [**IpTraceable**](/doc/ip_traceable.md) - inherited from Timestampable, sets IP address instead of timestamp -Currently these extensions support **Yaml**, **Annotation** and **Xml** mapping. Additional mapping drivers +All extensions support **Attribute**, **XML** and **Annotation** (deprecated) mapping. Additional mapping drivers can be easily implemented using Mapping extension to handle the additional metadata mapping. -**Note:** Please note, that xml mapping needs to be in a different namespace, the declared namespace for +### Version Compatibility + +* DBAL: `^3.2` (for all the extensions) or `^4.0` (for all the extensions, except **Loggable**) +* ORM: `^2.14` or `^3.0` +* MongoDB ODM: `^2.3` + +If you are setting up the Entity Manager without a framework, see the [example](/example/em.php) to prevent issues like #1310 + +### XML Mapping + +XML mapping needs to be in a different namespace, the declared namespace for Doctrine extensions is http://gediminasm.org/schemas/orm/doctrine-extensions-mapping So root node now looks like this: -**Note:** Use 2.1.x tag in order to use extensions based on Doctrine2.1.x versions. Currently -master branch is based on 2.2.x versions and may not work with 2.1.x - ```xml @@ -75,56 +91,37 @@ XML mapping xsd schemas are also versioned and can be used by version suffix: - 2.2.x version - **http://gediminasm.org/schemas/orm/doctrine-extensions-mapping-2-2** - 2.1.x version - **http://gediminasm.org/schemas/orm/doctrine-extensions-mapping-2-1** -### ODM MongoDB support - -List of extensions which support ODM +### Running Tests -- Translatable -- Sluggable -- Timestampable -- Blameable -- Loggable -- Translator -- Tree (Materialized Path strategy for now) -- References -- ReferenceIntegrity +To set up and run the tests, follow these steps: -All these extensions can be nested together and mapped in traditional ways - **annotations**, -**xml** or **yaml** +- Install [Docker](https://www.docker.com/) and ensure you have `docker compose` +- From the project root, run `docker compose up -d` to start containers in daemon mode +- Enter the container via `docker compose exec php bash` (you are now in the root directory: `/var/www`) +- Install Composer dependencies via `composer install` +- Run the tests: `vendor/bin/phpunit` -### Running the tests: +### Running the Example -**pdo-sqlite** extension is necessary. -To setup and run tests follow these steps: +To set up and run example, follow these steps: - go to the root directory of extensions -- download composer: `wget https://getcomposer.org/composer.phar` -- install dev libraries: `php composer.phar install` -- run: `bin/phpunit -c tests` -- optional - run mongodb service if targeting mongo tests - -### Running the example: - -To setup and run example follow these steps: - -- go to the root directory of extensions -- download composer: `wget https://getcomposer.org/composer.phar` -- install dev libraries: `php composer.phar install` +- [download composer](https://getcomposer.org/download/) +- install dev libraries: `composer install` - edit `example/em.php` and configure your database on top of the file -- run: `./example/bin/console` or `php example/bin/console` for console commands -- run: `./example/bin/console orm:schema-tool:create` to create schema -- run: `php example/run.php` to run example +- run: `php example/bin/console` or `php example/bin/console` for console commands +- run: `php example/bin/console orm:schema-tool:create` to create the schema +- run: `php example/bin/console app:print-category-translation-tree` to run the example to print the category translation tree -### Contributors: +### Contributors -Thanks to [everyone participating](http://github.com/l3pp4rd/DoctrineExtensions/contributors) in -the development of these great Doctrine2 extensions! +Thanks to [everyone participating](https://github.com/doctrine-extensions/DoctrineExtensions/contributors) in +the development of these great Doctrine extensions! And especially ones who create and maintain new extensions: -- Lukas Botsch [lbotsch](http://github.com/lbotsch) -- Gustavo Adrian [comfortablynumb](http://github.com/comfortablynumb) -- Boussekeyt Jules [gordonslondon](http://github.com/gordonslondon) -- Kudryashov Konstantin [everzet](http://github.com/everzet) +- Lukas Botsch [lbotsch](https://github.com/lbotsch) +- Gustavo Adrian [comfortablynumb](https://github.com/comfortablynumb) +- Boussekeyt Jules [gordonslondon](https://github.com/gordonslondon) +- Kudryashov Konstantin [everzet](https://github.com/everzet) - David Buchmann [dbu](https://github.com/dbu) - diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000000..188e6d0ba6 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,32 @@ +services: + php: + build: + context: . + target: php + dockerfile: ./.docker/php/Dockerfile + args: + PHP_VERSION: ${PHP_VERSION:-8.5-cli} + volumes: + - .:/var/www + working_dir: /var/www + environment: + MONGODB_SERVER: 'mongodb://mongodb:27017' + tty: true + stdin_open: true + init: true + + mysql: + image: mysql:8.0 + healthcheck: + test: ["CMD-SHELL", "mysql -h mysql --user=root --password=$${MYSQL_ROOT_PASSWORD} -e status"] + interval: 10s + timeout: 2s + retries: 5 + environment: + MYSQL_ROOT_PASSWORD: de_root_password + MYSQL_DATABASE: de_testing + MYSQL_USER: de_user + MYSQL_PASSWORD: de_password + + mongodb: + image: mongo diff --git a/composer.json b/composer.json index 6a6c7ffced..b03176a53f 100644 --- a/composer.json +++ b/composer.json @@ -1,14 +1,17 @@ { "name": "gedmo/doctrine-extensions", + "description": "Doctrine behavioral extensions", + "license": "MIT", "type": "library", - "description": "Doctrine2 behavioral extensions", "keywords": [ "behaviors", - "doctrine2", + "doctrine", "extensions", "gedmo", "sluggable", "loggable", + "odm", + "orm", "translatable", "tree", "nestedset", @@ -17,8 +20,6 @@ "blameable", "uploadable" ], - "homepage": "http://gediminasm.org/", - "license": "MIT", "authors": [ { "name": "Gediminas Morkevicius", @@ -33,36 +34,75 @@ "email": "david@liip.ch" } ], + "homepage": "http://gediminasm.org/", "support": { - "email": "gediminas.morkevicius@gmail.com", - "wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc" + "issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues", + "docs": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc" }, "require": { - "php": ">=5.3.2", - "behat/transliterator": "~1.0", - "doctrine/common": "~2.4" + "php": "^7.4 || ^8.0", + "doctrine/collections": "^1.2 || ^2.0", + "doctrine/deprecations": "^1.0", + "doctrine/event-manager": "^1.2 || ^2.0", + "doctrine/persistence": "^2.2 || ^3.0 || ^4.0", + "psr/cache": "^1 || ^2 || ^3", + "psr/clock": "^1", + "symfony/cache": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/string": "^5.4 || ^6.4 || ^7.3 || ^8.0" }, "require-dev": { - "doctrine/mongodb-odm": ">=1.0.2", - "doctrine/orm": ">=2.5.0", - "doctrine/common": ">=2.5.0", - "symfony/yaml": "~2.6", - "phpunit/phpunit": "~4.4", - "phpunit/phpunit-mock-objects": "~2.3" + "behat/transliterator": "^1.2", + "doctrine/annotations": "^1.13 || ^2.0", + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/common": "^2.13 || ^3.0", + "doctrine/dbal": "^3.7 || ^4.0", + "doctrine/doctrine-bundle": "^2.3 || ^3.0", + "doctrine/mongodb-odm": "^2.3", + "doctrine/orm": "^2.20 || ^3.3", + "friendsofphp/php-cs-fixer": "^3.89", + "nesbot/carbon": "^2.71 || ^3.0", + "phpstan/phpstan": "^2.1.31", + "phpstan/phpstan-doctrine": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.3", + "phpunit/phpunit": "^9.6", + "rector/rector": "^2.2.6", + "symfony/console": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/doctrine-bridge": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/phpunit-bridge": "^6.4 || ^7.3 || ^8.0", + "symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/yaml": "^5.4 || ^6.4 || ^7.3 || ^8.0" + }, + "conflict": { + "behat/transliterator": "<1.2 || >=2.0", + "doctrine/annotations": "<1.13 || >=3.0", + "doctrine/common": "<2.13 || >=4.0", + "doctrine/dbal": "<3.7 || >=5.0", + "doctrine/mongodb-odm": "<2.3 || >=3.0", + "doctrine/orm": "<2.20 || >=3.0 <3.3 || >=4.0" }, "suggest": { "doctrine/mongodb-odm": "to use the extensions with the MongoDB ODM", "doctrine/orm": "to use the extensions with the ORM" }, "autoload": { - "psr-0": { "Gedmo\\": "lib/" } + "psr-4": { + "Gedmo\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Gedmo\\Tests\\": "tests/Gedmo/" + } }, "config": { - "bin-dir": "bin" + "sort-packages": true }, "extra": { "branch-alias": { - "dev-master": "2.4.x-dev" + "dev-main": "3.x-dev" } + }, + "scripts": { + "fix-cs": "php-cs-fixer fix --config=.php-cs-fixer.dist.php" } } diff --git a/doc/annotations.md b/doc/annotations.md index c09514c31b..602702aefc 100644 --- a/doc/annotations.md +++ b/doc/annotations.md @@ -1,567 +1,1511 @@ -# Annotation reference +# Annotations Reference -Bellow you will find all annotation descriptions used in these extensions. -There will be introduction on usage with examples. For more detailed usage -on extensions, refer to their specific documentation. +> [!IMPORTANT] +> Support for annotations is deprecated and will be removed in 4.0. PHP 8 users are encouraged to migrate and use [attributes](./attributes.md) instead of annotations. To use annotations, you will need the [`doctrine/annotations`](https://www.doctrine-project.org/projects/annotations.html) library. -Content: +Below you will a reference for annotations supported in this extensions library. +There will be introduction on usage with examples. For more detailed usage of each +extension, refer to the extension's documentation page. -- Best [practices](#em-setup) for setting up -- [Tree](#gedmo-tree) -- [Translatable](#gedmo-translatable) -- [Sluggable](#gedmo-sluggable) -- [Timestampable](#gedmo-timestampable) -- [Loggable](#gedmo-loggable) +## Index -## Annotation mapping +- [Blameable Extension](#blameable-extension) +- [IP Traceable Extension](#ip-traceable-extension) +- [Loggable Extension](#loggable-extension) +- [Reference Integrity Extension](#reference-integrity-extension) +- [References Extension](#references-extension) +- [Sluggable Extension](#sluggable-extension) +- [Soft Deleteable Extension](#soft-deleteable-extension) +- [Sortable Extension](#sortable-extension) +- [Timestampable Extension](#timestampable-extension) +- [Translatable Extension](#translatable-extension) +- [Tree Extension](#tree-extension) +- [Uploadable Extension](#uploadable-extension) -Starting from **doctrine2.1.x** versions you have to import all used annotations -by an **use** statement, see example bellow: +## Reference -``` php -namespace MyApp\Entity; +### Blameable Extension -use Gedmo\Mapping\Annotation as Gedmo; // this will be like an alias for Gedmo extensions annotations -use Doctrine\ORM\Mapping\Id; // includes single annotation -use Doctrine\ORM\Mapping as ORM; // alias for doctrine ORM annotations +The below annotations are used to configure the [Blameable extension](./blameable.md). + +#### `@Gedmo\Mapping\Annotation\Blameable` + +The `Blameable` annotation is a property annotation used to identify fields which are updated to show information +about the last user to update the mapped object. A blameable field may have either a string value or a one-to-many +relationship with another entity. + +Required Attributes: + +- **on** - By default, the annotation configures the property to be updated when an object is updated; + this can be set to one of \[`change`, `create`, `update`\] + +Optional Attributes: + +- **field** - An optional list of properties to limit updates to the blameable field; this option is only + used when the **on** option is set to "change" and can be a dot separated path to indicate + properties on a related object are watched (i.e. `user.email` to reference the `$email` property + of the `$user` relation on this object) + +- **value** - An optional value to require the configured **field** to match to update the blameable field; + this option is only used when the **on** option is set to "change" + +> [!WARNING] +> When both the **field** and **value** options are set, the **field** can only be set to a single field; checking the value against multiple fields is not supported at this time + +Examples: + +```php + [!WARNING] +> When both the **field** and **value** options are set, the **field** can only be set to a single field; checking the value against multiple fields is not supported at this time + +Examples: + +```php + - -## Best practices for setting up with annotations - -New annotation reader does not depend on any namespaces, for that reason you can use -single reader instance for whole project. The example bellow shows how to setup the -mapping and listeners: - -**Note:** using this repository you can test and check the [example demo configuration](https://github.com/l3pp4rd/DoctrineExtensions/blob/master/example/em.php) - -``` php -addDriver($annotationDriver, 'Entity'); +### Reference Integrity Extension -// general ORM configuration -$config = new Doctrine\ORM\Configuration; -$config->setProxyDir(sys_get_temp_dir()); -$config->setProxyNamespace('Proxy'); -$config->setAutoGenerateProxyClasses(false); // this can be based on production config. -// register metadata driver -$config->setMetadataDriverImpl($driverChain); -// use our already initialized cache driver -$config->setMetadataCacheImpl($cache); -$config->setQueryCacheImpl($cache); +The below annotations are used to configure the [Reference Integrity extension](./reference_integrity.md). -// create event manager and hook preferred extension listeners -$evm = new Doctrine\Common\EventManager(); -// gedmo extension listeners, remove which are not used +> [!WARNING] +> This extension is only usable with the Doctrine MongoDB ODM -// sluggable -$sluggableListener = new Gedmo\Sluggable\SluggableListener; -// you should set the used annotation reader to listener, to avoid creating new one for mapping drivers -$sluggableListener->setAnnotationReader($cachedAnnotationReader); -$evm->addEventSubscriber($sluggableListener); +#### `@Gedmo\Mapping\Annotation\ReferenceIntegrity` -// tree -$treeListener = new Gedmo\Tree\TreeListener; -$treeListener->setAnnotationReader($cachedAnnotationReader); -$evm->addEventSubscriber($treeListener); +The `ReferenceIntegrity` annotation is a property annotation used to identify fields where referential integrity +should be checked. The annotation must be used on a property which references another document, and the reference +configuration must have a `mappedBy` configuration. -// loggable, not used in example -$loggableListener = new Gedmo\Loggable\LoggableListener; -$loggableListener->setAnnotationReader($cachedAnnotationReader); -$evm->addEventSubscriber($loggableListener); +Required Attributes: -// timestampable -$timestampableListener = new Gedmo\Timestampable\TimestampableListener; -$timestampableListener->setAnnotationReader($cachedAnnotationReader); -$evm->addEventSubscriber($timestampableListener); +- **value** - The type of action to take for the reference, must be one of \[`nullify`, `pull`, `restrict`\] -// translatable -$translatableListener = new Gedmo\Translatable\TranslatableListener; -// current translation locale should be set from session or hook later into the listener -// most important, before entity manager is flushed -$translatableListener->setTranslatableLocale('en'); -$translatableListener->setDefaultLocale('en'); -$translatableListener->setAnnotationReader($cachedAnnotationReader); -$evm->addEventSubscriber($translatableListener); +Example: -// sortable, not used in example -$sortableListener = new Gedmo\Sortable\SortableListener; -$sortableListener->setAnnotationReader($cachedAnnotationReader); -$evm->addEventSubscriber($sortableListener); +```php +addEventSubscriber(new Doctrine\DBAL\Event\Listeners\MysqlSessionInit()); -// DBAL connection -$connection = array( - 'driver' => 'pdo_mysql', - 'host' => '127.0.0.1', - 'dbname' => 'test', - 'user' => 'root', - 'password' => '' -); -// Finally, create entity manager -$em = Doctrine\ORM\EntityManager::create($connection, $config, $evm); + /** + * @ODM\ReferenceOne(targetDocument="App\Document\User", mappedBy="articles") + * @Gedmo\ReferenceIntegrity("nullify") + */ + public ?User $author = null; + + /** + * @var Collection + * + * @ODM\ReferenceMany(targetDocument="App\Document\Comment", mappedBy="article") + * @Gedmo\ReferenceIntegrity("nullify") + */ + private Collection $comments; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } +} ``` -**Note:** that symfony2 StofDoctrineExtensionsBundle does it automatically this -way you will maintain a single instance of annotation reader. It relates only -to doctrine-common-2.1.x branch and newer. +### References Extension + +The below annotations are used to configure the [References extension](./references.md). - +#### `@Gedmo\Mapping\Annotation\ReferenceOne` -## Tree annotations +The `ReferenceOne` annotation is a property annotation used to create a reference between two objects in different +databases or object managers. This is similar to a `ReferenceOne` relationship in the MongoDB ODM. -Tree can use different adapters. Currently **Tree** extension supports **NestedSet** -and **Closure** strategies which has a difference for annotations used. Note, that -tree will automatically map indexes which are considered necessary for best performance. +Required Attributes: -### @Gedmo\Mapping\Annotation\Tree (required for all tree strategies) +- **value** - The type of action to take for the reference, must be one of \[`nullify`, `pull`, `restrict`\] -**class** annotation +- **type** - The type of object manager to use for the reference, must be one of \[`document`, `entity`\] -Is the main identificator of tree used for domain object which should **act as Tree**. +- **class** - The class name of the object to reference -**options:** +Optional Attributes: -- **type** - (string) _optional_ default: **nested** +- **identifier** - The name of the property to store the identifier value in -example: +- **inversedBy** - The name of the property on the inverse side of the reference -``` php +Example: + +```php + * + * @Gedmo\ReferenceMany(type="entity", class="App\Entity\Comment", mappedBy="comments") + */ + private Collection $comments; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } +} ``` -### @Gedmo\Mapping\Annotation\TreeLeft (required for nested tree) +#### `@Gedmo\Mapping\Annotation\ReferenceManyEmbed` + +The `ReferenceManyEmbed` annotation is a property annotation used to create a reference between two objects in different +databases or object managers. This is similar to a `ReferenceMany` relationship in the MongoDB ODM. + +Required Attributes: + +- **value** - The type of action to take for the reference, must be one of \[`nullify`, `pull`, `restrict`\] + +- **type** - The type of object manager to use for the reference, must be one of \[`document`, `entity`\] + +- **class** - The class name of the object to reference -**property** annotation +Optional Attributes: -This annotation forces to specify the **left** field, which will be used for generation -of nestedset left values. Property must be **integer** type. +- **identifier** - The name of the property to store the identifier value in -example: +Example: -``` php +```php + * + * @Gedmo\ReferenceManyEmbed(type="entity", class="App\Entity\Comment", identifier="metadata.commentId") + */ + private Collection $comments; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } +} ``` -### @Gedmo\Mapping\Annotation\TreeRight (required for nested tree) +### Sluggable Extension + +The below annotations are used to configure the [Sluggable extension](./sluggable.md). + +#### `@Gedmo\Mapping\Annotation\Slug` + +The `Slug` annotation is a property annotation used to identify the field the slug is stored to. + +Required Attributes: + +- **fields** - A list of fields on the object to use for generating a slug, this must be a non-empty list of strings -**property** annotation +Optional Attributes: -This annotation forces to specify the **right** field, which will be used for generation -of nestedset right values. Property must be **integer** type. +- **updatable** - Flag indicating the slug can be automatically updated if any of the fields have changed, + defaults to `true` -example: +- **style** - The style to use while generating the slug, defaults to `default` (no style changes) and ignores + unsupported styles; supported styles are: + - `camel` - Converts the slug to a camel-case string + - `lower` - Converts the slug to a fully lowercased string + - `upper` - Converts the slug to a fully uppercased string -``` php +- **unique** - Flag indicating the slug must be unique, defaults to `true` + +- **unique_base** - The name of the object property that should be used as a key when doing a uniqueness check, + can only be set when the **unique** flag is `true` + +- **separator** - The separator to use between words in the slug, defaults to `-` + +- **prefix** - An optional prefix for the generated slug + +- **suffix** - An optional suffix for the generated slug + +- **handlers** - A list of `@Gedmo\Mapping\Annotation\SlugHandler` annotations used to further customize the slug + generator behavior + +Basic Example: + +```php +### Timestampable Extension + +The below annotations are used to configure the [Timestampable extension](./timestampable.md). -## Translatable annotations +#### `@Gedmo\Mapping\Annotation\Timestampable` -Translatable additionally can have unmapped property, which would override the -locale used by listener. +The `Timestampable` annotation is a property annotation used to identify fields which are updated to record the +timestamp of the update the mapped object. A timestampable field must be a field supporting a `DateTimeInterface`. -### @Gedmo\Mapping\Annotation\TranslationEntity (optional) +Required Attributes: -**class** annotation +- **on** - By default, the annotation configures the property to be updated when an object is updated; + this can be set to one of \[`change`, `create`, `update`\] -This class annotation can force translatable to use **personal Entity** to store -translations. In large tables this can be very handy. +Optional Attributes: -**options:** +- **field** - An optional list of properties to limit updates to the timestampable field; this option is only + used when the **on** option is set to "change" and can be a dot separated path to indicate + properties on a related object are watched (i.e. `user.email` to reference the `$email` property + of the `$user` relation on this object) -- **class** - (string) _required_ +- **value** - An optional value to require the configured **field** to match to update the timestampable field; + this option is only used when the **on** option is set to "change" -example: +> [!WARNING] +> When both the **field** and **value** options are set, the **field** can only be set to a single field; checking the value against multiple fields is not supported at this time -``` php +Examples: + +```php [!TIP] +> Although not strictly required, translation classes are encouraged to extend from the `AbstractPersonalTranslation` or `AbstractTranslation` classes in the `Gedmo\Translatable\\MappedSuperclass` namespace + +Example: + +```php + [!WARNING] +> Only the `materializedPath` tree type is supported for the MongoDB ODM at this time + +Optional Attributes: + +- **activateLocking** - Indicates that a materialized path tree should be locked during write transactions, + defaults to true + +- **lockingTimeout** - The time (in seconds) for the lock timeout, defaults to 3 + +Example: + +```php +##### `@Gedmo\Mapping\Annotation\TreeParent` -## Sluggable annotations +The `TreeParent` annotation is a property annotation used to identify the relationship for a tree object to its parent. +All tree objects **MUST** have this annotation and the annotation must be defined on a many-to-one relationship. -Sluggable ensures unique slugs and correct length of the slug. It also uses utf8 to ascii -table map to create correct ascii slugs. +Example: -### @Gedmo\Mapping\Annotation\Slug (required) +```php + [!WARNING] +> This strategy is only usable with the Doctrine ORM -### Slug handlers: +##### `@Gedmo\Mapping\Annotation\TreeClosure` -- Gedmo\Sluggable\Handler\TreeSlugHandler - transforms a tree slug into path based, example "food/fruits/apricots/king-apricots" -- Gedmo\Sluggable\Handler\RelativeSlugHandler - takes a relation slug and prefixes the slug, example "singers/michael-jackson" -in order to synchronize updates regarding the relation changes, you will need to hood **InversedRelativeSlugHandler** to the relation mentioned. -- Gedmo\Sluggable\Handler\InversedRelativeSlugHandler - updates prefixed slug for an inversed relation which is mapped by **RelativeSlugHandler** +The `TreeClosure` annotation is a class annotation used to configure a closure domain object +for a closure tree strategy. -examples: +Required Attributes: -``` php +- **class** - The class to be used for the closure domain object, this must be a + subclass of `Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure` + +Example: + +```php [!WARNING] +> This annotation is only usable with the Doctrine MongoDB ODM + +The `TreeLockTime` annotation is a property annotation used to identify the property that the tree lock time is +stored in. This must be a date field. + +Example: -``` php +```php +#### Nested Tree Strategy + +> [!WARNING] +> This strategy is only usable with the Doctrine ORM + +##### `@Gedmo\Mapping\Annotation\TreeRoot` + +The `TreeRoot` annotation is a property annotation used to identify the relationship for a tree object to its root node. +This is an optional annotation for nested trees which improves performance and allows supporting multiple trees within +a single table, and when used, the annotation must be defined on a many-to-one relationship. + +This annotation will use an **integer** type field to specify the root of tree. This way +updating tree will cost less because each root will act as separate tree. + +Optional Attributes: -## Timestampable annotations +- **identifierMethod** - Allows specifying a method on the related object to call to retrieve the identifier; + when not configured, the root property value will be used -Timestampable will update date fields on create, update or property change. If you set/force -date manually it will not update it. +Example: -### @Gedmo\Mapping\Annotation\Timestampable (required) +```php + +### Uploadable Extension + +The below annotations are used to configure the [Uploadable extension](./uploadable.md). -## Loggable annotations +#### `@Gedmo\Mapping\Annotation\Uploadable` -Loggable is used to log all actions made on annotated object class, it logs insert, update -and remove actions for a username which currently is logged in for instance. -Further more, it stores all **Versioned** property changes in the log which allows -a version management implementation for this object. +The `Uploadable` annotation is a class annotation used to identify objects which can store information about +uploaded files. -### @Gedmo\Mapping\Annotation\Loggable (required) +Optional Attributes: -**class** annotation +- **allowOverwrite** - Flag indicating that an existing uploaded file can be overwritten, defaults to `false` -This class annotation marks object as being loggable and logs all actions being done to -this class records. +- **appendNumber** - Flag indicating that a number should be appended to the filename when the **allowOverwrite** + attribute is `true` and a file already exists, defaults to `false` -**options:** +- **path** - The file path to store files for this uploadable at; this must be configured unless the **pathMethod** + attribute is configured or a default path is set on the uploadable listener -- **logEntryClass** - (string) _optional_ personal log storage class +- **pathMethod** - A method name on this class to use to retrieve the file path to store files for this uploadable; + this must be configured unless the **path** attribute is configured or a default path is set on + the uploadable listener -example: +- **callback** - A method name on this class to use to call after the file has been moved -``` php +- **filenameGenerator** - A filename generator to use when moving the uploaded file to be used to normalize/customize + the file name and defaults to `NONE`; supported styles are: + - `ALPHANUMERIC` - Normalizes the filename, leaving only alphanumeric characters + - `NONE` - No conversion + - `SHA1` - Creates a SHA1 hash of the filename + - A class name of a class implementing `Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorInterface` + +- **maxSize** - An optional maximum file size for this uploadable object; defaults to `0` + +- **allowedTypes** - An optional list of allowed MIME types for this uploadable object; + cannot be set at the same time as the **disallowedTypes** attribute + +- **disallowedTypes** - An optional list of disallowed MIME types for this uploadable object + cannot be set at the same time as the **allowedTypes** attribute + +Example: + +```php [!WARNING] +> When both the **field** and **value** parameters are set, the **field** can only be set to a single field; checking the value against multiple fields is not supported at this time + +Examples: + +```php + [!WARNING] +> When both the **field** and **value** parameters are set, the **field** can only be set to a single field; checking the value against multiple fields is not supported at this time + +Examples: + +```php + [!WARNING] +> This extension is only usable with the Doctrine MongoDB ODM + +#### `#[Gedmo\Mapping\Annotation\ReferenceIntegrity]` + +The `ReferenceIntegrity` attribute is a property attribute used to identify fields where referential integrity +should be checked. The attribute must be used on a property which references another document, and the reference +configuration must have a `mappedBy` configuration. + +Required Parameters: + +- **value** - The type of action to take for the reference, must be one of \[`nullify`, `pull`, `restrict`\] + +Example: + +```php + + */ + #[ODM\ReferenceMany(targetDocument: Comment::class, mappedBy: 'article')] + #[Gedmo\ReferenceIntegrity('nullify')] + private Collection $comments; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } +} +``` + +### References Extension + +The below attributes are used to configure the [References extension](./references.md). + +#### `#[Gedmo\Mapping\Annotation\ReferenceOne]` + +The `ReferenceOne` attribute is a property attribute used to create a reference between two objects in different +databases or object managers. This is similar to a `ReferenceOne` relationship in the MongoDB ODM. + +Required Parameters: + +- **value** - The type of action to take for the reference, must be one of \[`nullify`, `pull`, `restrict`\] + +- **type** - The type of object manager to use for the reference, must be one of \[`document`, `entity`\] + +- **class** - The class name of the object to reference + +Optional Parameters: + +- **identifier** - The name of the property to store the identifier value in + +- **inversedBy** - The name of the property on the inverse side of the reference + +Example: + +```php + + */ + #[Gedmo\ReferenceMany(type: 'entity', class: Comment::class, mappedBy: 'comments')] + private Collection $comments; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } +} +``` + +#### `#[Gedmo\Mapping\Annotation\ReferenceManyEmbed]` + +The `ReferenceManyEmbed` attribute is a property attribute used to create a reference between two objects in different +databases or object managers. This is similar to a `ReferenceMany` relationship in the MongoDB ODM. + +Required Parameters: + +- **value** - The type of action to take for the reference, must be one of \[`nullify`, `pull`, `restrict`\] + +- **type** - The type of object manager to use for the reference, must be one of \[`document`, `entity`\] + +- **class** - The class name of the object to reference + +Optional Parameters: + +- **identifier** - The name of the property to store the identifier value in + +Example: + +```php + + */ + #[Gedmo\ReferenceManyEmbed(type: 'entity', class: Comment::class, identifier: 'metadata.commentId')] + private Collection $comments; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } +} +``` + +### Sluggable Extension + +The below attributes are used to configure the [Sluggable extension](./sluggable.md). + +#### `#[Gedmo\Mapping\Annotation\Slug]` + +The `Slug` attribute is a property attribute used to identify the field the slug is stored to. + +Required Parameters: + +- **fields** - A list of fields on the object to use for generating a slug, this must be a non-empty list of strings + +Optional Parameters: + +- **updatable** - Flag indicating the slug can be automatically updated if any of the fields have changed, + defaults to `true` + +- **style** - The style to use while generating the slug, defaults to `default` (no style changes) and ignores + unsupported styles; supported styles are: + - `camel` - Converts the slug to a camel-case string + - `lower` - Converts the slug to a fully lowercased string + - `upper` - Converts the slug to a fully uppercased string + +- **unique** - Flag indicating the slug must be unique, defaults to `true` + +- **unique_base** - The name of the object property that should be used as a key when doing a uniqueness check, + can only be set when the **unique** flag is `true` + +- **separator** - The separator to use between words in the slug, defaults to `-` + +- **prefix** - An optional prefix for the generated slug + +- **suffix** - An optional suffix for the generated slug + +- **handlers** - Deprecated to be removed in 4.0, this parameter is unused with attributes in favor of + the `SlugHandler` attribute + +Basic Example: + +```php + 'category', 'separator' => '/'])] + public ?string $slug = null; +} +``` + +#### `#[Gedmo\Mapping\Annotation\SlugHandlerOption]` + +> [!WARNING] +> The `SlugHandlerOption` attribute is deprecated and will be removed in 4.0. Using this attribute is not supported, and instead, the options can be configured directly in the `SlugHandler` attribute's **options** parameter. + +### Soft Deleteable Extension + +The below attributes are used to configure the [Soft Deleteable extension](./soft-deleteable.md). + +#### `#[Gedmo\Mapping\Annotation\SoftDeleteable]` + +The `SoftDeleteable` attribute is a class attribute used to identify objects which are soft deleteable. + +Required Parameters: + +- **fieldName** - The name of the property in which the soft delete timestamp is stored, defaults to `deletedAt`; + this field must be a field support a `DateTimeInterface` + +Optional Parameters: + +- **timeAware** - Flag indicating the object supports scheduled soft deletes, defaults to `false` + +- **hardDelete** - Flag indicating the object supports hard deletes, defaults to `true` + +Examples: + +```php + [!WARNING] +> When both the **field** and **value** parameters are set, the **field** can only be set to a single field; checking the value against multiple fields is not supported at this time + +Examples: + +```php + [!TIP] +> Although not strictly required, translation classes are encouraged to extend from the `AbstractPersonalTranslation` or `AbstractTranslation` classes in the `Gedmo\Translatable\\MappedSuperclass` namespace + +Example: + +```php + [!WARNING] +> Only the `materializedPath` tree type is supported for the MongoDB ODM at this time + +Optional Parameters: + +- **activateLocking** - Indicates that a materialized path tree should be locked during write transactions, + defaults to true + +- **lockingTimeout** - The time (in seconds) for the lock timeout, defaults to 3 + +Example: + +```php + [!WARNING] +> This strategy is only usable with the Doctrine ORM + +##### `#[Gedmo\Mapping\Annotation\TreeClosure]` + +The `TreeClosure` attribute is a class attribute used to configure a closure domain object +for a closure tree strategy. + +Required Parameters: + +- **class** - The class to be used for the closure domain object, this must be a + subclass of `Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure` + +Example: + +```php + [!WARNING] +> This attribute is only usable with the Doctrine MongoDB ODM + +The `TreeLockTime` attribute is a property attribute used to identify the property that the tree lock time is +stored in. This must be a date field. + +Example: + +```php + [!WARNING] +> This strategy is only usable with the Doctrine ORM + +##### `#[Gedmo\Mapping\Annotation\TreeRoot]` + +The `TreeRoot` attribute is a property attribute used to identify the relationship for a tree object to its root node. +This is an optional attribute for nested trees which improves performance and allows supporting multiple trees within +a single table, and when used, the attribute must be defined on a many-to-one relationship. + +This attribute will use an **integer** type field to specify the root of tree. This way +updating tree will cost less because each root will act as separate tree. + +Optional Parameters: + +- **identifierMethod** - Allows specifying a method on the related object to call to retrieve the identifier; + when not configured, the root property value will be used + +Example: + +```php +getEventManager()->addEventSubscriber($listener); +``` -**Symfony:** - -- **Blameable** is available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle) -for **Symfony2**, together with all other extensions - -This article will cover the basic installation and functionality of **Blameable** behavior - -Content: - -- [Including](#including-extension) the extension -- Entity [example](#entity-mapping) -- Document [example](#document-mapping) -- [Yaml](#yaml-mapping) mapping example -- [Xml](#xml-mapping) mapping example -- Advanced usage [examples](#advanced-examples) -- Using [Traits](#traits) - - - -## Setup and autoloading +Then, once your application has it available (i.e. after validating the authentication for your user during an HTTP request), +you can set a reference to the user to be blamed for changes. -Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup) -or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example) -on how to setup and use the extensions in most optimized way. +The user reference can be set through either an [actor provider service](./utils/actor-provider.md) or by calling the +listener's `setUserValue` method with a resolved user. - +> [!TIP] +> When an actor provider is given to the extension, any data set with the `setUserValue` method will be ignored. -## Blameable Entity example: +```php +// The $provider must be an implementation of Gedmo\Tool\ActorProviderInterface +$listener->setActorProvider($provider); -### Blameable annotations: -- **@Gedmo\Mapping\Annotation\Blameable** this annotation tells that this column is blameable -by default it updates this column on update. If column is not a string field or an association -it will trigger an exception. +// The $user can be either an object or a string +$listener->setUserValue($user); +``` -Available configuration options: +## Configuring Blameable Objects -- **on** - is main option and can be **create, update, change** this tells when it -should be updated -- **field** - only valid if **on="change"** is specified, tracks property or a list of properties for changes -- **value** - only valid if **on="change"** is specified and the tracked field is a single field (not an array), if the tracked field has this **value** -then it updates the blame +The blameable extension can be configured with [annotations](./annotations.md#blameable-extension), +[attributes](./attributes.md#blameable-extension), or XML configuration (matching the mapping of +your domain models). The full configuration for annotations and attributes can be reviewed in +the linked documentation. -**Note:** that Blameable interface is not necessary, except in cases there -you need to identify entity as being Blameable. The metadata is loaded only once then -cache is activated +The below examples show the simplest and default configuration for the extension, setting a field +when the model is updated. -Column is a string field: +### Attribute Configuration -``` php +```php id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function setBody($body) - { - $this->body = $body; - } - - public function getBody() - { - return $this->body; - } - - public function getCreatedBy() - { - return $this->createdBy; - } - - public function getUpdatedBy() - { - return $this->updatedBy; - } - - public function getContentChangedBy() - { - return $this->contentChangedBy; - } + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + public ?int $id = null; + + #[ORM\Column(type: Types::STRING)] + #[Gedmo\Blameable] + public ?string $updatedBy = null; } ``` -Column is an association: - -``` php - + - /** - * @var User $updatedBy - * - * @Gedmo\Blameable(on="update") - * @ORM\ManyToOne(targetEntity="Path\To\Entity\User") - * @ORM\JoinColumn(name="updated_by", referencedColumnName="id") - */ - private $updatedBy; + + + + - /** - * @var User $contentChangedBy - * - * @Gedmo\Blameable(on="change", fields={"title", "body"}) - * @ORM\ManyToOne(targetEntity="Path\To\Entity\User") - * @ORM\JoinColumn(name="content_changed_by", referencedColumnName="id") - */ - private $contentChangedBy; - - public function getId() - { - return $this->id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function setBody($body) - { - $this->body = $body; - } - - public function getBody() - { - return $this->body; - } - - public function getCreatedBy() - { - return $this->createdBy; - } - - public function getUpdatedBy() - { - return $this->updatedBy; - } - - public function getContentChangedBy() - { - return $this->contentChangedBy; - } -} + + + + + ``` - +### Annotation Configuration -## Blameable Document example: +> [!NOTE] +> Support for annotations is deprecated and will be removed in 4.0. -``` php +```php id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function getCreatedBy() - { - return $this->createdBy; - } - - public function getUpdatedBy() - { - return $this->updatedBy; - } + public ?string $updatedBy = null; } ``` -Now on update and creation these annotated fields will be automatically updated - - - -## Yaml mapping example: - -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** - -``` ---- -Entity\Article: - type: entity - table: articles - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - createdBy: - type: string - gedmo: - blameable: - on: create - updatedBy: - type: string - gedmo: - blameable: - on: update -``` - - - -## Xml mapping example +### Supported Field Types -``` xml - - +The blameable extension supports the following field types for a blameable field: - - - - +- String (`string`, or when using the ORM and DBAL, `ascii_string`) +- Integer (`int` only) +- UUID or ULID (requires a third party package providing a `uuid` or `ulid` Doctrine type; popular packages for + the ORM and DBAL include [`ramsey/uuid-doctrine`](https://github.com/ramsey/uuid-doctrine) and + [`symfony/doctrine-bridge`](https://github.com/symfony/doctrine-bridge)) +- A many-to-one association (ORM) or reference many reference (MongoDB ODM) - - - - - - - - - +## Using Traits - - - - +The blameable extension provides traits which can be used to quickly add fields, and optionally the mapping configuration, +for a created by and updated by username to be updated for the **create** and **update** blameable actions. These traits are +provided as a convenience for a common configuration, for other use cases it is suggested you add your own fields and configurations. - -``` +- `Gedmo\Blameable\Traits\Blameable` adds a `$createdBy` and `$updatedBy` property, with getters and setters +- `Gedmo\Blameable\Traits\BlameableDocument` adds a `$createdBy` and `$updatedBy` property, with getters and setters + and mapping annotations and attributes for the MongoDB ODM +- `Gedmo\Blameable\Traits\BlameableEntity` adds a `$createdBy` and `$updatedBy` property, with getters and setters + and mapping annotations and attributes for the ORM - +## Logging Changes For Specific Actions -## Advanced examples: +In addition to supporting logging the user for general create and update actions, the extension can also be configured to +log the user who made a change for specific fields or values. -### Using dependency of property changes +### Single Field Changed To Specific Value -Add another entity which would represent Article Type: +For example, we want to record the user who published an article on a news site. To do this, we add a field to our object +and configure it using the **change** action, specifying the field and value we want it to match. -``` php +```php id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Gedmo\Blameable(on: 'change', field: 'published', value: true)] + public ?string $publishedBy = null; } ``` -Now update the Article Entity to reflect publishedBy on Type change: +The change action can also be configured to watch for changes on related objects using a dot notation path. In this example, +we log the user who updated the article and placed it into an archived category. -``` php +```php type = $type; - } - - public function getId() - { - return $this->id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function getCreatedBy() - { - return $this->createdBy; - } - - public function getUpdatedBy() - { - return $this->updatedBy; - } - - public function getPublishedBy() - { - return $this->publishedBy; - } + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Gedmo\Blameable(on: 'change', field: 'category.archived', value: true)] + public ?string $archivedBy = null; } ``` -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** - -``` ---- -Entity\Article: - type: entity - table: articles - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - createdBy: - type: string - gedmo: - blameable: - on: create - updatedBy: - type: string - gedmo: - blameable: - on: update - publishedBy: - type: string - gedmo: - blameable: - on: change - field: type.title - value: Published - manyToOne: - type: - targetEntity: Entity\Type - inversedBy: articles -``` +### One of Many Fields Changed -Now few operations to get it all done: +The extension can also update a blameable field when using the **change** action by specifying a list of fields to watch. +This also supports the dotted path notation, allowing you to watch changes on the model itself as well as related data. -``` php +```php setTitle('My Article'); - -$em->persist($article); -$em->flush(); -// article: $createdBy, $updatedBy were set - -$type = new Type; -$type->setTitle('Published'); - -$article = $em->getRepository('Entity\Article')->findByTitle('My Article'); -$article->setType($type); - -$em->persist($article); -$em->persist($type); -$em->flush(); -// article: $publishedBy, $updatedBy were set - -$article->getPublishedBy(); // the user that published this article -``` - -Easy like that, any suggestions on improvements are very welcome +namespace App\Entity; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; - - -## Traits - -You can use blameable traits for quick **createdBy** **updatedBy** string definitions -when using annotation mapping. -There is also a trait without annotations for easy integration purposes. +#[ORM\Entity] +class Article +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + public ?int $id = null; -**Note:** this feature is only available since php **5.4.0**. And you are not required -to use the Traits provided by extensions. + #[ORM\ManyToOne(targetEntity: Category::class)] + public ?Category $category = null; -``` php - [!TIP] +> This guide is written using the Laminas MVC quick start as the foundation. + +Assuming you have already [created your Laminas application](https://docs.laminas.dev/laminas-mvc/quick-start/), +the next step will be to ensure you've installed this library and the Doctrine libraries you will need. + +For Doctrine MongoDB ODM users, this Composer command will install all required dependencies: + +```shell +composer require doctrine/doctrine-module doctrine/doctrine-mongo-odm-module doctrine/mongodb-odm gedmo/doctrine-extensions +``` + +For Doctrine ORM users, this Composer command will install all required dependencies: + +```shell +composer require doctrine/dbal doctrine/doctrine-module doctrine/doctrine-orm-module doctrine/orm gedmo/doctrine-extensions +``` + +## Registering Extension Listeners + +At the heart of the Doctrine Extensions library are the listeners which enable each extension. The below example demonstrates +how to register and enable all listeners provided by this library. + +### Extensions Compatible with all Managers + +```php + [ + 'invokables' => [ + 'gedmo.mapping.driver.attribute' => AttributeReader::class, + ], + 'factories' => [ + 'gedmo.listener.blameable' => function (ContainerInterface $container, string $requestedName): BlameableListener { + $listener = new BlameableListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + return $listener; + }, + 'gedmo.listener.ip_traceable' => function (ContainerInterface $container, string $requestedName): IpTraceableListener { + $listener = new IpTraceableListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + return $listener; + }, + 'gedmo.listener.loggable' => function (ContainerInterface $container, string $requestedName): LoggableListener { + $listener = new LoggableListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + return $listener; + }, + 'gedmo.listener.sluggable' => function (ContainerInterface $container, string $requestedName): SluggableListener { + $listener = new SluggableListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + return $listener; + }, + 'gedmo.listener.soft_deleteable' => function (ContainerInterface $container, string $requestedName): SoftDeleteableListener { + $listener = new SoftDeleteableListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + // If your application uses a PSR-20 clock, you can provide it to this listener by uncommenting the below line + // $listener->setClock($container->get(ClockInterface::class)); + + return $listener; + }, + 'gedmo.listener.sortable' => function (ContainerInterface $container, string $requestedName): SortableListener { + $listener = new SortableListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + return $listener; + }, + 'gedmo.listener.timestampable' => function (ContainerInterface $container, string $requestedName): TimestampableListener { + $listener = new TimestampableListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + // If your application uses a PSR-20 clock, you can provide it to this listener by uncommenting the below line + // $listener->setClock($container->get(ClockInterface::class)); + + return $listener; + }, + 'gedmo.listener.translatable' => function (ContainerInterface $container, string $requestedName): TranslatableListener { + $listener = new TranslatableListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + // If your application uses a PSR-20 clock, you can provide it to this listener by uncommenting the below line + // $listener->setClock($container->get(ClockInterface::class)); + + return $listener; + }, + ], + ], + 'doctrine' => [ + 'eventmanager' => [ + 'orm_default' => [ + 'subscribers' => [ + 'gedmo.listener.blameable', + 'gedmo.listener.ip_traceable', + 'gedmo.listener.loggable', + 'gedmo.listener.sluggable', + 'gedmo.listener.soft_deleteable', + 'gedmo.listener.sortable', + 'gedmo.listener.timestampable', + 'gedmo.listener.translatable', + 'gedmo.listener.tree', + ], + ], + ], + ], +]; +``` + +### Extensions Compatible with MongoDB ODM Only + +```php + [ + 'factories' => [ + 'gedmo.listener.reference_integrity' => function (ContainerInterface $container, string $requestedName): ReferenceIntegrityListener { + $listener = new ReferenceIntegrityListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + return $listener; + }, + 'gedmo.listener.references' => function (ContainerInterface $container, string $requestedName): ReferencesListener { + $listener = new ReferencesListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + return $listener; + }, + ], + ], + 'doctrine' => [ + 'eventmanager' => [ + 'odm_default' => [ + 'subscribers' => [ + 'gedmo.listener.reference_integrity', + 'gedmo.listener.references', + ], + ], + ], + ], +]; +``` + +### Extensions Compatible with ORM Only + +```php + [ + 'factories' => [ + 'gedmo.listener.uploadable' => function (ContainerInterface $container, string $requestedName): UploadableListener { + $listener = new UploadableListener(); + + // This call configures the listener to use the attribute driver service created above; if using annotations, you will need to provide the appropriate service instead + $listener->setAnnotationReader($container->get('gedmo.mapping.driver.attribute')); + + return $listener; + }, + ], + ], + 'doctrine' => [ + 'eventmanager' => [ + 'orm_default' => [ + 'subscribers' => [ + 'gedmo.listener.uploadable', + ], + ], + ], + ], +]; +``` + +## Registering Mapping Configuration + +When using the [Loggable](../loggable.md), [Translatable](../translatable.md), or [Tree](../tree.md) extensions, you will +need to register the mappings for these extensions to your object managers. + +> [!NOTE] +> These extensions only provide mappings through annotations or attributes, with support for annotations being deprecated. If using annotations, you will need to ensure the [`doctrine/annotations`](https://www.doctrine-project.org/projects/annotations.html) library is installed and configured. + +### MongoDB ODM Mapping + +> [!IMPORTANT] +> The tree extension does NOT have any objects to map when using the MongoDB ODM. + +The below example shows a configuration adding all available mappings to the default document manager. + +```php + [ + 'driver' => [ + 'gedmo.odm_driver' => [ + 'class' => AttributeDriver::class, // If your application is using annotations, use the AnnotationDriver class instead + 'paths' => [ + '/path/to/vendor/gedmo/doctrine-extensions/src/Loggable/Document', + '/path/to/vendor/gedmo/doctrine-extensions/src/Translatable/Document', + ], + ], + 'odm_default' => [ + 'drivers' => [ + 'gedmo.odm_driver', // Adds the mapping driver created above to the default mapping chain + ], + ], + ], + ], +]; +``` + +### ORM Mapping + +The below example shows a configuration adding all available mappings to the default entity manager. + +```php + [ + 'driver' => [ + 'gedmo.orm_driver' => [ + 'class' => AttributeDriver::class, // If your application is using annotations, use the AnnotationDriver class instead + 'paths' => [ + '/path/to/vendor/gedmo/doctrine-extensions/src/Loggable/Entity', + '/path/to/vendor/gedmo/doctrine-extensions/src/Translatable/Entity', + '/path/to/vendor/gedmo/doctrine-extensions/src/Tree/Entity', + ], + ], + 'orm_default' => [ + 'drivers' => [ + 'gedmo.orm_driver', // Adds the mapping driver created above to the default mapping chain + ], + ], + ], + ], +]; +``` + +To verify your configuration, you can use the `orm:info` command from the `doctrine-module` CLI tool to make sure the entities are registered. + +```sh +$ vendor/bin/doctrine-module orm:info + Found X mapped entities: + + [OK] Gedmo\Loggable\Entity\LogEntry + [OK] Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry + [OK] Gedmo\Translatable\Entity\Translation + [OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation + [OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation + [OK] Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure +``` + +## Registering Filters + +### Soft Deleteable Filter + +When using the [Soft Deleteable](../soft-deleteable.md) extension, a filter is available which allows configuring whether +soft-deleted objects are included in query results. + +> [!NOTE] +> The default configuration in the Laminas modules does not enable the filters. To use these filters, you will need to enable them separately. + +#### MongoDB ODM Filter Configuration + +The below example shows a configuration adding the filter to the default document manager. To enable the filter, +you can follow the [Filters documentation guide](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/reference/filters.html#disabling-enabling-filters-and-setting-parameters). + +```php + [ + 'configuration' => [ + 'odm_default' => [ + 'filters' => [ + 'soft-deleteable' => SoftDeleteableFilter::class, + ], + ], + ], + ], +]; +``` + +#### ORM Filter Configuration + +The below example shows a configuration adding the filter to the default entity manager. To enable the filter, +you can follow the [Filters documentation guide](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/filters.html#disabling-enabling-filters-and-setting-parameters). + +```php + [ + 'configuration' => [ + 'orm_default' => [ + 'filters' => [ + 'soft-deleteable' => SoftDeleteableFilter::class, + ], + ], + ], + ], +]; +``` + +## Configuring Extensions via Event Listeners + +When using the [Blameable](../blameable.md), [IP Traceable](../ip_traceable.md), [Loggable](../loggable.md), or +[Translatable](../translatable.md) extensions, to work correctly, they require extra information that must be set +at runtime. + +**Help Improve This Documentation** + +Pull requests are welcome to expand this section of the documentation. diff --git a/doc/frameworks/laravel.md b/doc/frameworks/laravel.md new file mode 100644 index 0000000000..0afe9e9834 --- /dev/null +++ b/doc/frameworks/laravel.md @@ -0,0 +1,5 @@ +# Integrate the Doctrine Extensions in Laravel + +When using the Laravel Doctrine package with the Doctrine ORM, you can use the [Laravel Doctrine Extensions](https://www.laraveldoctrine.org/docs/current/extensions) +package to add this library to your application. Please review the extensions package documentation linked earlier for +detailed instructions. diff --git a/doc/frameworks/symfony.md b/doc/frameworks/symfony.md new file mode 100644 index 0000000000..8b41066092 --- /dev/null +++ b/doc/frameworks/symfony.md @@ -0,0 +1,481 @@ +# Integrate the Doctrine Extensions in Symfony + +This guide will demonstrate how to integrate the Doctrine Extensions library into a Symfony application. + +> [!TIP] +> We recommend using the [`StofDoctrineExtensionsBundle`](https://symfony.com/bundles/StofDoctrineExtensionsBundle/current/index.html) which handles this integration for you. + +## Index + +- [Getting Started](#getting-started) +- [Registering Extension Listeners](#registering-extension-listeners) +- [Registering Mapping Configuration](#registering-mapping-configuration) +- [Registering Filters](#registering-filters) +- [Configuring Extensions via Event Subscribers](#configuring-extensions-via-event-subscribers) + +## Getting Started + +Assuming you have already [created your Symfony application](https://symfony.com/doc/current/getting_started/index.html), +the next step will be to ensure you've installed this library and the Doctrine libraries you will need. + +For Doctrine MongoDB ODM users, this Composer command will install all required dependencies: + +```shell +composer require doctrine/mongodb-odm doctrine/mongodb-odm-bundle gedmo/doctrine-extensions +``` + +For Doctrine ORM users, this Composer command will install all required dependencies: + +```shell +composer require doctrine/dbal doctrine/doctrine-bundle doctrine/orm gedmo/doctrine-extensions +``` + +## Registering Extension Listeners + +At the heart of the Doctrine Extensions library are the listeners which enable each extension. The below example demonstrates +how to register and enable all listeners provided by this library. + +### Extensions Compatible with all Managers + +> [!NOTE] +> This example shows the configuration when using the ORM and `DoctrineBundle` with a single default entity manager. When using the MongoDB ODM and `DoctrineMongoDBBundle`, the tag name should be `doctrine_mongodb.odm.event_listener` instead of `doctrine.event_listener`. When using an application with multiple managers, a separate tag is needed with the `connection` attribute for each connection. + +```yaml +services: + # Attribute mapping driver for the Doctrine Extension listeners + gedmo.mapping.driver.attribute: + class: Gedmo\Mapping\Driver\AttributeReader + + # Gedmo Blameable Extension Listener + gedmo.listener.blameable: + class: Gedmo\Blameable\BlameableListener + tags: + - { name: doctrine.event_listener, event: 'prePersist' } + - { name: doctrine.event_listener, event: 'onFlush' } + - { name: doctrine.event_listener, event: 'loadClassMetadata' } + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] + + # Gedmo IP Traceable Extension Listener + gedmo.listener.ip_traceable: + class: Gedmo\IpTraceable\IpTraceableListener + tags: + - { name: doctrine.event_listener, event: 'prePersist' } + - { name: doctrine.event_listener, event: 'onFlush' } + - { name: doctrine.event_listener, event: 'loadClassMetadata' } + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] + + # Gedmo Loggable Extension Listener + gedmo.listener.loggable: + class: Gedmo\Loggable\LoggableListener + tags: + - { name: doctrine.event_listener, event: 'onFlush' } + - { name: doctrine.event_listener, event: 'loadClassMetadata' } + - { name: doctrine.event_listener, event: 'postPersist' } + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] + + # Gedmo Sluggable Extension Listener + gedmo.listener.sluggable: + class: Gedmo\Sluggable\SluggableListener + tags: + - { name: doctrine.event_listener, event: 'onFlush' } + - { name: doctrine.event_listener, event: 'loadClassMetadata' } + - { name: doctrine.event_listener, event: 'prePersist' } + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] + + # Gedmo Soft Deleteable Extension listener + gedmo.listener.soft_deleteable: + class: Gedmo\SoftDeleteable\SoftDeleteableListener + tags: + - { name: doctrine.event_listener, event: 'loadClassMetadata' } + - { name: doctrine.event_listener, event: 'onFlush' } + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] + # The `clock` service was introduced in Symfony 6.2; if using an older Symfony version, you can either comment this call or provide your own PSR-20 Clock implementation + - [ setClock, [ '@clock' ] ] + + # Gedmo Sortable Extension listener + gedmo.listener.sortable: + class: Gedmo\Sortable\SortableListener + tags: + - { name: doctrine.event_listener, event: 'onFlush' } + - { name: doctrine.event_listener, event: 'loadClassMetadata' } + - { name: doctrine.event_listener, event: 'prePersist' } + - { name: doctrine.event_listener, event: 'postPersist' } + - { name: doctrine.event_listener, event: 'preUpdate' } + - { name: doctrine.event_listener, event: 'postRemove' } + - { name: doctrine.event_listener, event: 'postFlush' } + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] + + # Gedmo Timestampable Extension Listener + gedmo.listener.timestampable: + class: Gedmo\Timestampable\TimestampableListener + tags: + - { name: doctrine.event_listener, event: 'prePersist' } + - { name: doctrine.event_listener, event: 'onFlush' } + - { name: doctrine.event_listener, event: 'loadClassMetadata' } + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] + # The `clock` service was introduced in Symfony 6.2; if using an older Symfony version, you can either comment this call or provide your own PSR-20 Clock implementation + - [ setClock, [ '@clock' ] ] + + # Gedmo Translatable Extension Listener + gedmo.listener.translatable: + class: Gedmo\Translatable\TranslatableListener + tags: + - { name: doctrine.event_listener, event: 'postLoad' } + - { name: doctrine.event_listener, event: 'postPersist' } + - { name: doctrine.event_listener, event: 'preFlush' } + - { name: doctrine.event_listener, event: 'onFlush' } + - { name: doctrine.event_listener, event: 'loadClassMetadata' } + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] + # The Kernel's `locale` parameter is used to configure the default locale for the extension + - [ setDefaultLocale, [ '%locale%' ] ] + + # Gedmo Tree Extension Listener + gedmo.listener.tree: + class: Gedmo\Tree\TreeListener + tags: + - { name: doctrine.event_listener, event: 'prePersist'} + - { name: doctrine.event_listener, event: 'preUpdate'} + - { name: doctrine.event_listener, event: 'preRemove'} + - { name: doctrine.event_listener, event: 'onFlush'} + - { name: doctrine.event_listener, event: 'loadClassMetadata'} + - { name: doctrine.event_listener, event: 'postPersist'} + - { name: doctrine.event_listener, event: 'postUpdate'} + - { name: doctrine.event_listener, event: 'postRemove'} + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] +``` + +### Extensions Compatible with MongoDB ODM Only + +> [!NOTE] +> This example shows the configuration when using the MongoDB ODM and `DoctrineMongoDBBundle` with a single default document manager. When using an application with multiple managers, a separate tag is needed with the `connection` attribute for each connection. + +```yaml +services: + # Gedmo Reference Integrity Extension Listener + gedmo.listener.reference_integrity: + class: Gedmo\ReferenceIntegrity\ReferenceIntegrityListener + tags: + - { name: doctrine_mongodb.odm.event_listener, event: 'loadClassMetadata' } + - { name: doctrine_mongodb.odm.event_listener, event: 'preRemove' } + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] + + # Gedmo References Extension Listener + gedmo.listener.references: + class: Gedmo\References\ReferencesListener + tags: + - { name: doctrine_mongodb.odm.event_listener, event: 'postLoad' } + - { name: doctrine_mongodb.odm.event_listener, event: 'loadClassMetadata' } + - { name: doctrine_mongodb.odm.event_listener, event: 'prePersist' } + - { name: doctrine_mongodb.odm.event_listener, event: 'preUpdate' } + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] +``` + +### Extensions Compatible with ORM Only + +> [!NOTE] +> This example shows the configuration when using the ORM and `DoctrineBundle` with a single default entity manager. When using an application with multiple managers, a separate tag is needed with the `connection` attribute for each connection. + +```yaml +services: + # Gedmo Uploadable Extension Listener + gedmo.listener.uploadable: + class: Gedmo\Uploadable\UploadableListener + tags: + - { name: doctrine.event_listener, event: 'loadClassMetadata'} + - { name: doctrine.event_listener, event: 'preFlush'} + - { name: doctrine.event_listener, event: 'onFlush'} + - { name: doctrine.event_listener, event: 'postFlush'} + calls: + # Uncomment the below call if using attributes, and comment the call for the annotation reader + # - [ setAnnotationReader, [ '@gedmo.mapping.driver.attribute' ] ] + # The `annotation_reader` service was deprecated in Symfony 6.4 and removed in Symfony 7.0 + - [ setAnnotationReader, [ '@annotation_reader' ] ] +``` + +## Registering Mapping Configuration + +When using the [Loggable](../loggable.md), [Translatable](../translatable.md), or [Tree](../tree.md) extensions, you will +need to register the mappings for these extensions to your object managers. + +> [!NOTE] +> These extensions only provide mappings through annotations or attributes, with support for annotations being deprecated. If using annotations, you will need to ensure the [`doctrine/annotations`](https://www.doctrine-project.org/projects/annotations.html) library is installed and configured. + +### MongoDB ODM Mapping + +> [!IMPORTANT] +> The tree extension does NOT have any objects to map when using the MongoDB ODM. + +The below example shows a configuration adding all available mappings to the default document manager. + +```yaml +doctrine_mongodb: + document_managers: + default: + mappings: + loggable: + type: attribute # or annotation + alias: GedmoLoggable + prefix: Gedmo\Loggable\Document + dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Loggable/Document" + is_bundle: false + translatable: + type: attribute # or annotation + alias: GedmoTranslatable + prefix: Gedmo\Translatable\Document + dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Translatable/Document" + is_bundle: false +``` + +To verify your configuration, you can use the `doctrine:mongodb:mapping:info` command to make sure the entities are registered. + +```shell +$ bin/console doctrine:mongodb:mapping:info + Found X documents mapped in document manager default: + [OK] Gedmo\Loggable\Document\LogEntry + [OK] Gedmo\Loggable\Document\MappedSuperclass\AbstractLogEntry + [OK] Gedmo\Translatable\Document\MappedSuperclass\AbstractPersonalTranslation + [OK] Gedmo\Translatable\Document\MappedSuperclass\AbstractTranslation + [OK] Gedmo\Translatable\Document\Translation +``` + +### ORM Mapping + +The below example shows a configuration adding all available mappings to the default entity manager. + +```yaml +doctrine: + orm: + default: + mappings: + loggable: + type: attribute # or annotation + alias: GedmoLoggable + prefix: Gedmo\Loggable\Entity + dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Loggable/Entity" + is_bundle: false + translatable: + type: attribute # or annotation + alias: GedmoTranslatable + prefix: Gedmo\Translatable\Entity + dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Translatable/Entity" + is_bundle: false + tree: + type: attribute # or annotation + alias: GedmoTree + prefix: Gedmo\Tree\Entity + dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Tree/Entity" + is_bundle: false +``` + +To verify your configuration, you can use the `doctrine:mapping:info` command to make sure the entities are registered. + +```shell +$ bin/console doctrine:mapping:info + Found X mapped entities: + [OK] Gedmo\Loggable\Entity\LogEntry + [OK] Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry + [OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation + [OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation + [OK] Gedmo\Translatable\Entity\Translation + [OK] Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure +``` + +## Registering Filters + +### Soft Deleteable Filter + +When using the [Soft Deleteable](../soft-deleteable.md) extension, a filter is available which allows configuring whether +soft-deleted objects are included in query results. + +> [!NOTE] +> The default configuration in the Symfony bundles does not enable the filters. These examples show how to globally enable them. + +#### MongoDB ODM Filter Configuration + +The below example shows a configuration adding the filter to the default document manager. + +```yaml +doctrine_mongodb: + document_managers: + default: + filters: + 'soft-deleteable': + class: Gedmo\SoftDeleteable\Filter\ODM\SoftDeleteableFilter + enabled: true +``` + +#### ORM Filter Configuration + +The below example shows a configuration adding the filter to the default entity manager. + +```yaml +doctrine: + orm: + default: + filters: + 'soft-deleteable': + class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter + enabled: true +``` + +## Configuring Extensions via Event Subscribers + +When using the [Blameable](../blameable.md), [IP Traceable](../ip_traceable.md), [Loggable](../loggable.md), or +[Translatable](../translatable.md) extensions, to work correctly, they require extra information that must be set +at runtime, typically during the `kernel.request` event. The below example is an event subscriber class which configures +all of these extensions. + +```php + [ + ['configureBlameableListener'], // Must run after the user is authenticated + ['configureIpTraceableListener', 512], // Runs early since this only requires the Request object + ['configureLoggableListener'], // Must run after the user is authenticated + ['configureTranslatableListener'], // Must run after the locale is configured + ], + ]; + } + + /** + * Configures the blameable listener using the currently authenticated user + */ + public function configureBlameableListener(RequestEvent $event): void + { + // Only applies to the main request + if (!$event->isMainRequest()) { + return; + } + + // If the required security component services weren't provided, there's nothing we can do + if (null === $this->authorizationChecker || null === $this->tokenStorage) { + return; + } + + $token = $this->tokenStorage->getToken(); + + // Only set the user information if there is a token in storage and it represents an authenticated user + if (null !== $token && $this->authorizationChecker->isGranted('IS_AUTHENTICATED')) { + $this->blameableListener->setUserValue($token->getUser()); + } + } + + /** + * Configures the IP traceable listener using the current request + */ + public function configureIpTraceableListener(RequestEvent $event): void + { + // Only applies to the main request + if (!$event->isMainRequest()) { + return; + } + + $ip = $event->getRequest()->getClientIp(); + + // Only set the IP address if available + if (null !== $ip) { + $this->ipTraceableListener->setIpValue($ip); + } + } + + /** + * Configures the loggable listener using the currently authenticated user + */ + public function configureLoggableListener(RequestEvent $event): void + { + // Only applies to the main request + if (!$event->isMainRequest()) { + return; + } + + // If the required security component services weren't provided, there's nothing we can do + if (null === $this->authorizationChecker || null === $this->tokenStorage) { + return; + } + + $token = $this->tokenStorage->getToken(); + + // Only set the user information if there is a token in storage and it represents an authenticated user + if (null !== $token && $this->authorizationChecker->isGranted('IS_AUTHENTICATED')) { + $this->loggableListener->setUsername($token->getUser()); + } + } + + /** + * Configures the translatable listener using the request locale + */ + public function configureTranslatableListener(RequestEvent $event): void + { + $this->translatableListener->setTranslatableLocale($event->getRequest()->getLocale()); + } +} +``` diff --git a/doc/ip_traceable.md b/doc/ip_traceable.md index 9fd94d6248..34a22d0309 100644 --- a/doc/ip_traceable.md +++ b/doc/ip_traceable.md @@ -1,660 +1,266 @@ -# IpTraceable behavior extension for Doctrine 2 +# IP Traceable Behavior Extension for Doctrine -**IpTraceable** behavior will automate the update of IP trace -on your Entities or Documents. It works through annotations and can update -fields on creation, update, property subset update, or even on specific property value change. +The **IP Traceable** behavior automates the update of IP addresses on your Doctrine objects. -This is very similar to Timestampable but sets a string. +## Index -Note that you need to set the IP on the IpTraceableListener (unless you use the -Symfony2 extension which does automatically assign the current request IP). +- [Getting Started](#getting-started) +- [Configuring IP Traceable Objects](#configuring-ip-traceable-objects) +- [Using Traits](#using-traits) +- [Logging Changes For Specific Actions](#logging-changes-for-specific-actions) +## Getting Started -Features: +The IP traceable behavior can be added to a supported Doctrine object manager by registering its event subscriber +when creating the manager. -- Automatic predefined ip field update on creation, update, property subset update, and even on record property changes -- ORM and ODM support using same listener -- Specific annotations for properties, and no interface required -- Can react to specific property or relation changes to specific value -- Can be nested with other behaviors -- Annotation, Yaml and Xml mapping support for extensions - - -**Symfony:** - -- **IpTraceable** is not yet available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle) -for **Symfony2**, together with all other extensions - -This article will cover the basic installation and functionality of **IpTraceable** behavior - -Content: - -- [Including](#including-extension) the extension -- Entity [example](#entity-mapping) -- Document [example](#document-mapping) -- [Yaml](#yaml-mapping) mapping example -- [Xml](#xml-mapping) mapping example -- Advanced usage [examples](#advanced-examples) -- Using [Traits](#traits) - - +```php +use Gedmo\IpTraceable\IpTraceableListener; -## Setup and autoloading +$listener = new IpTraceableListener(); -Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup) -or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example) -on how to setup and use the extensions in most optimized way. +// The $om is either an instance of the ORM's entity manager or the MongoDB ODM's document manager +$om->getEventManager()->addEventSubscriber($listener); +``` - +Then, once your application has it available, you can set the IP address to be recorded. The IP address can be set through +either an [IP address provider service](./utils/ip-address-provider.md) or by calling the listener's `setIpValue` method. -## IpTraceable Entity example: +```php +// The $provider must be an implementation of Gedmo\Tool\IpAddressProviderInterface +$listener->setIpAddressProvider($provider); -### IpTraceable annotations: -- **@Gedmo\Mapping\Annotation\IpTraceable** this annotation tells that this column is ipTraceable -by default it updates this column on update. If column is not a string field it will trigger an exception. +$listener->setIpValue('127.0.0.1'); +``` -Available configuration options: +## Configuring IP Traceable Objects -- **on** - is main option and can be **create, update, change** this tells when it -should be updated -- **field** - only valid if **on="change"** is specified, tracks property or a list of properties for changes -- **value** - only valid if **on="change"** is specified and the tracked field is a single field (not an array), if the tracked field has this **value** -then it updates the trace +The IP traceable extension can be configured with [annotations](./annotations.md#ip-traceable-extension), +[attributes](./attributes.md#ip-traceable-extension), or XML configuration (matching the mapping of +your domain models). The full configuration for annotations and attributes can be reviewed in +the linked documentation. -**Note:** that IpTraceable interface is not necessary, except in cases there -you need to identify entity as being IpTraceable. The metadata is loaded only once then -cache is activated +The below examples show the simplest and default configuration for the extension, setting a field +when the model is updated. -Column is a string field: +### Attribute Configuration -``` php +```php id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function setBody($body) - { - $this->body = $body; - } - - public function getBody() - { - return $this->body; - } - - public function getCreatedFromIp() - { - return $this->createdFromIp; - } - - public function getUpdatedFromIp() - { - return $this->updatedFromIp; - } - - public function getContentChangedFromIp() - { - return $this->contentChangedFromIp; - } -} -``` - - - - -## IpTraceable Document example: - -``` php -id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function getCreatedFromIp() - { - return $this->createdFromIp; - } - - public function getUpdatedFromIp() - { - return $this->updatedFromIp; - } + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + public ?int $id = null; + + #[ORM\Column(type: Types::STRING)] + #[Gedmo\IpTraceable] + public ?string $updatedFromIp = null; } ``` -Now on update and creation these annotated fields will be automatically updated - - - -## Yaml mapping example: +### XML Configuration -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** - -``` ---- -Entity\Article: - type: entity - table: articles - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - createdFromIp: - type: string - length: 45 - nullable: true - gedmo: - ipTraceable: - on: create - updatedFromIp: - type: string - length: 45 - nullable: true - gedmo: - ipTraceable: - on: update -``` - - - -## Xml mapping example - -``` xml +```xml - + - - - - + - - - - - - - - ``` - - -## Advanced examples: +### Annotation Configuration -### Using dependency of property changes +> [!NOTE] +> Support for annotations is deprecated and will be removed in 4.0. -Add another entity which would represent Article Type: - -``` php +```php id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } -} -``` - -Now update the Article Entity to reflect publishedFromIp on Type change: - -``` php -type = $type; - } - - public function getId() - { - return $this->id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function getCreatedFromIp() - { - return $this->createdFromIp; - } - - public function getUpdatedFromIp() - { - return $this->updatedFromIp; - } - - public function getPublishedFromIp() - { - return $this->publishedFromIp; - } + public ?string $updatedFromIp = null; } ``` -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** +### Supported Field Types -``` ---- -Entity\Article: - type: entity - table: articles - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - createdFromIp: - type: string - length: 45 - nullable: true - gedmo: - ipTraceable: - on: create - updatedFromIp: - type: string - length: 45 - nullable: true - gedmo: - ipTraceable: - on: update - publishedFromIp: - type: string - length: 45 - nullable: true - gedmo: - ipTraceable: - on: change - field: type.title - value: Published - manyToOne: - type: - targetEntity: Entity\Type - inversedBy: articles -``` +The IP traceable extension supports the following field types for the IP address field: -Now few operations to get it all done: +- String (`string`, or when using the ORM and DBAL, `ascii_string`) -``` php -setTitle('My Article'); - -$em->persist($article); -$em->flush(); -// article: $createdFromIp, $updatedFromIp were set - -$type = new Type; -$type->setTitle('Published'); - -$article = $em->getRepository('Entity\Article')->findByTitle('My Article'); -$article->setType($type); +## Using Traits -$em->persist($article); -$em->persist($type); -$em->flush(); -// article: $publishedFromIp, $updatedFromIp were set +The IP traceable extension provides traits which can be used to quickly add fields, and optionally the mapping configuration, +for a created by and updated by IP address to be updated for the **create** and **update** actions. These traits are +provided as a convenience for a common configuration, for other use cases it is suggested you add your own fields and configurations. -$article->getPublishedFromIp(); // the IP that published this article -``` - -Easy like that, any suggestions on improvements are very welcome +- `Gedmo\IpTraceable\Traits\IpTraceable` adds a `$createdFromIp` and `$updatedFromIp` property, with getters and setters +- `Gedmo\IpTraceable\Traits\IpTraceableDocument` adds a `$createdFromIp` and `$updatedFromIp` property, with getters and setters + and mapping annotations and attributes for the MongoDB ODM +- `Gedmo\IpTraceable\Traits\IpTraceableEntity` adds a `$createdFromIp` and `$updatedFromIp` property, with getters and setters + and mapping annotations and attributes for the ORM +## Logging Changes For Specific Actions - +In addition to supporting logging the user for general create and update actions, the extension can also be configured to +log the IP address who made a change for specific fields or values. -## Traits +### Single Field Changed To Specific Value -You can use IpTraceable traits for quick **createdFromIp** **updatedFromIp** string definitions -when using annotation mapping. -There is also a trait without annotations for easy integration purposes. +For example, we want to record the IP address who published an article on a news site. To do this, we add a field to our object +and configure it using the **change** action, specifying the field and value we want it to match. -**Note:** this feature is only available since php **5.4.0**. And you are not required -to use the Traits provided by extensions. - -``` php +```php ipTraceableListener = $ipTraceableListener; - $this->request = $request; - } + #[ORM\Column(type: Types::STRING)] + #[Gedmo\IpTraceable] + public ?string $updatedFromIp = null; /** - * Set the username from the security context by listening on core.request - * - * @param GetResponseEvent $event + * Field to track the IP address who archived this article. */ - public function onKernelRequest(GetResponseEvent $event) - { - if (null === $this->request) { - return; - } - - // If you use a cache like Varnish, you may want to set a proxy to Request::getClientIp() method - // $this->request->setTrustedProxies(array('127.0.0.1')); - - // $ip = $_SERVER['REMOTE_ADDR']; - $ip = $this->request->getClientIp(); - - if (null !== $ip) { - $this->ipTraceableListener->setIpValue($ip); - } - } - - public static function getSubscribedEvents() - { - return array( - KernelEvents::REQUEST => 'onKernelRequest', - ); - } + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Gedmo\IpTraceable(on: 'change', field: 'category.archived', value: true)] + public ?string $archivedFromIp = null; } - ``` -### Configuration for services.xml +### One of Many Fields Changed -``` xml - +The extension can also update a traceable field when using the **change** action by specifying a list of fields to watch. +This also supports the dotted path notation, allowing you to watch changes on the model itself as well as related data. - +```php + - Acme\DemoBundle\EventListener\IpTraceListener - +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; - - - ... +#[ORM\Entity] +class Article +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + public ?int $id = null; - - - - - - + #[ORM\ManyToOne(targetEntity: Category::class)] + public ?Category $category = null; - - - - - + #[ORM\Column(type: Types::STRING, nullable: true)] + public ?string $metaDescription = null; - - + #[ORM\Column(type: Types::STRING, nullable: true)] + public ?string $metaKeywords = null; + + /** + * Field to track the IP address who last made any change to this article. + */ + #[ORM\Column(type: Types::STRING)] + #[Gedmo\IpTraceable] + public ?string $updatedFromIp = null; + /** + * Field to track the IP address who last modified this article's SEO metadata. + */ + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Gedmo\IpTraceable(on: 'change', field: ['metaDescription', 'metaKeywords', 'category.metaDescription', 'category.metaKeywords'])] + public ?string $seoMetadataChangedFromIp = null; +} ``` diff --git a/doc/loggable.md b/doc/loggable.md index bdf07a927b..3f0b9dc55e 100644 --- a/doc/loggable.md +++ b/doc/loggable.md @@ -1,266 +1,249 @@ -# Loggable behavioral extension for Doctrine2 +# Loggable Behavior Extension for Doctrine -**Loggable** behavior tracks your record changes and is able to -manage versions. +The **Loggable** behavior adds support for logging changes to and restoring prior versions of your Doctrine objects. -Features: +> [!NOTE] +> The Loggable extension is NOT compatible with `doctrine/dbal` 4.0 or later -- Automatic storage of log entries in database -- ORM and ODM support using same listener -- Can be nested with other behaviors -- Objects can be reverted to previous versions -- Annotation, Yaml and Xml mapping support for extensions +## Index -Update **2011-04-04** +- [Getting Started](#getting-started) +- [Configuring Loggable Objects](#configuring-loggable-objects) +- [Customizing The Log Entry Model](#customizing-the-log-entry-model) +- [Object Repositories](#object-repositories) + - [Fetching a Model's Log Entries](#fetching-a-models-log-entries) + - [Revert a Model to a Previous Version](#revert-a-model-to-a-previous-version) -- Made single listener, one instance can be used for any object manager -and any number of them +## Getting Started -**Portability:** +The loggable behavior can be added to a supported Doctrine object manager by registering its event subscriber +when creating the manager. -- **Loggable** is now available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle) -ported to **Symfony2** by **Christophe Coevoet**, together with all other extensions +```php +use Gedmo\Loggable\LoggableListener; -This article will cover the basic installation and functionality of **Loggable** -behavior +$listener = new LoggableListener(); -Content: - -- [Including](#including-extension) the extension -- Entity [example](#entity-mapping) -- Document [example](#document-mapping) -- [Yaml](#yaml-mapping) mapping example -- [Xml](#xml-mapping) mapping example -- Basic usage [examples](#basic-examples) - - +// The $om is either an instance of the ORM's entity manager or the MongoDB ODM's document manager +$om->getEventManager()->addEventSubscriber($listener); +``` -## Setup and autoloading +Then, once your application has it available (i.e. after validating the authentication for your user during an HTTP request), +you can set a reference to the user who performed actions on a loggable model. -Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup) -or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example) -on how to setup and use the extensions in most optimized way. +The user reference can be set through either an [actor provider service](./utils/actor-provider.md) or by calling the +listener's `setUsername` method with a resolved user. -### Loggable annotations: +> [!TIP] +> When an actor provider is given to the extension, any data set with the `setUsername` method will be ignored. -- **@Gedmo\Mapping\Annotation\Loggable(logEntryClass="my\class")** this class annotation -will store logs to optionally specified **logEntryClass**. You will still need to specify versioned fields with the following annotation. -- **@Gedmo\Mapping\Annotation\Versioned** tracks annotated property for changes +```php +// The $provider must be an implementation of Gedmo\Tool\ActorProviderInterface +$listener->setActorProvider($provider); -### Loggable username: +// The $user can be either an object or a string +$listener->setUsername($user); +``` -In order to set the username, when adding the loggable listener you need to set it this way: +## Configuring Loggable Objects -``` php -$loggableListener = new Gedmo\Loggable\LoggableListener; -$loggableListener->setAnnotationReader($cachedAnnotationReader); -$loggableListener->setUsername('admin'); -$evm->addEventSubscriber($loggableListener); -``` - +The loggable extension can be configured with [annotations](./annotations.md#loggable-extension), +[attributes](./attributes.md#loggable-extension), or XML configuration (matching the mapping of +your domain models). The full configuration for annotations and attributes can be reviewed in +the linked documentation. -## Loggable Entity example: +The below examples show the simplest and default configuration for the extension, logging changes for defined fields. -**Note:** that Loggable interface is not necessary, except in cases there -you need to identify entity as being Loggable. The metadata is loaded only once when -cache is active +### Attribute Configuration -``` php +```php id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } + #[ORM\Column(type: Types::BOOLEAN)] + public bool $published = false; + + #[ORM\Column(type: Types::STRING)] + #[Gedmo\Versioned] + public ?string $title = null; } ``` - +### XML Configuration + +```xml + + + + + + + + + + + + + + + + + +``` + +### Annotation Configuration -## Loggable Document example: +> [!NOTE] +> Support for annotations is deprecated and will be removed in 4.0. -``` php +```php title; - } - - public function getId() - { - return $this->id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } + public ?string $title = null; } ``` - +## Customizing The Log Entry Model -## Yaml mapping example +When configuring loggable models, you are able to specify a custom model to be used for the log entries for objects +of that type using the `logEntryClass` parameter: -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** +### Attribute Configuration -``` ---- -Entity\Article: - type: entity - table: articles - gedmo: - loggable: -# using specific personal LogEntryClass class: - logEntryClass: My\LogEntry -# without specifying the LogEntryClass class: -# loggable: true - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - gedmo: - - versioned - content: - type: text -``` +```php + +#[ORM\Entity] +#[Gedmo\Loggable(logEntryClass: ArticleLogEntry::class)] +class Article +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + public ?int $id = null; +} +``` -## Xml mapping example +### XML Configuration -``` xml +```xml - - + - - - - - - - - - - + ``` - +A custom model must implement `Gedmo\Loggable\LogEntryInterface`. For convenience, we recommend extending from +`Gedmo\Loggable\Entity\MappedSuperClass\AbstractLogEntry` for Doctrine ORM users or +`Gedmo\Loggable\Document\MappedSuperClass\AbstractLogEntry` for Doctrine MongoDB ODM users, which provides a default +mapping configuration for each object manager. -## Basic usage examples: +## Object Repositories -``` php -setTitle('my title'); -$em->persist($article); -$em->flush(); -``` +The loggable extension includes a `Doctrine\Persistence\ObjectRepository` implementation for each supported object manager +that provides out-of-the-box features for all log entry models. When creating custom models, you are welcome to extend +from either `Gedmo\Loggable\Entity\Repository\LogEntryRepository` for Doctrine ORM users or +`Gedmo\Loggable\Document\Repository\LogEntryRepository` for Doctrine MongoDB ODM users to provide these features. -This inserted an article and inserted the logEntry for it, which contains -all new changeset. In case if there is **OneToOne or ManyToOne** relation, -it will store only identifier of that object to avoid storing proxies +### Fetching a Model's Log Entries -Now lets update our article: +The repository classes provide a `getLogEntries` method which allows fetching the list of log entries for a given model. -``` php -find('Entity\Article', 1 /*article id*/); -$article->setTitle('my new title'); -$em->persist($article); -$em->flush(); -``` +```php +use App\Entity\Article; +use Doctrine\ORM\EntityManagerInterface; +use Gedmo\Loggable\Entity\LogEntry; +use Gedmo\Loggable\Entity\Repository\LogEntryRepository; +use Gedmo\Loggable\LoggableListener; -This updated an article and inserted the logEntry for update action with new changeset -Now lets revert it to previous version: +/** @var EntityManagerInterface $em */ -``` php -getRepository('Gedmo\Loggable\Entity\LogEntry'); // we use default log entry class -$article = $em->find('Entity\Article', 1 /*article id*/); +// Load our loggable model +$article = $em->find(Article::class, 1); + +// Next, get the LogEntry repository +/** @var LogEntryRepository $repo */ +$repo = $em->getRepository(LogEntry::class); + +// Lastly, get the article's log entries $logs = $repo->getLogEntries($article); -/* $logs contains 2 logEntries */ -// lets revert to first version -$repo->revert($article, 1/*version*/); -// notice article is not persisted yet, you need to persist and flush it -echo $article->getTitle(); // prints "my title" -$em->persist($article); -$em->flush(); -// if article had changed relation, it would be reverted also. ``` -Easy like that, any suggestions on improvements are very welcome +### Revert a Model to a Previous Version + +The repository classes provide a `revert` method which allows reverting a model to a previous version. The repository +will incrementally revert back to the version specified (for example, a model is currently on version 5, and you want to +revert to version 2, it will restore the state of version 4, then version 3, and finally, version 2). + +```php +use App\Entity\Article; +use Doctrine\ORM\EntityManagerInterface; +use Gedmo\Loggable\Entity\LogEntry; +use Gedmo\Loggable\Entity\Repository\LogEntryRepository; +use Gedmo\Loggable\LoggableListener; + +/** @var EntityManagerInterface $em */ + +// Load our loggable model +$article = $em->find(Article::class, 1); + +// Next, get the LogEntry repository +/** @var LogEntryRepository $repo */ +$repo = $em->getRepository(LogEntry::class); + +// We are now able to revert to an older version +$repo->revert($article, 2); +``` diff --git a/doc/mapping.md b/doc/mapping.md index 636103c127..5cb48cc378 100644 --- a/doc/mapping.md +++ b/doc/mapping.md @@ -1,18 +1,17 @@ -# Mapping extension for Doctrine2 +# Mapping extension for Doctrine **Mapping** extension makes it easy to map additional metadata for event listeners. -It supports **Yaml**, **Xml** and **Annotation** drivers which will be chosen depending on +It supports **Attribute**, **Xml** and **Annotation** drivers which will be chosen depending on currently used mapping driver for your domain objects. **Mapping** extension also provides abstraction layer of **EventArgs** to make it possible to use single listener for different object managers like **ODM** and **ORM**. Features: -- Mapping drivers for annotation and yaml +- Mapping drivers for annotation - Conventional extension points for metadata extraction and object manager abstraction -- Public [Mapping repository](http://github.com/l3pp4rd/DoctrineExtensions "Mapping extension on Github") is available on github -- Last update date: **2012-01-02** +- Public [Mapping repository](https://github.com/doctrine-extensions/DoctrineExtensions "Mapping extension on Github") is available on github This article will cover the basic installation and usage of **Mapping** extension @@ -31,8 +30,8 @@ Content: ## Setup and autoloading -Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup) -or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example) +Read the [documentation](./annotations.md#em-setup) +or check the [example code](../example) on how to setup and use the extensions in most optimized way. @@ -56,7 +55,7 @@ project Now you can use any namespace autoloader class and register this namespace. We will use Doctrine\Common\ClassLoader for instance: -``` php +```php getFieldMapping($field); - if ($mapping['type'] != 'string') { + if (($mapping->type ?? $mapping['type']) != 'string') { throw new \Exception("Only strings can be encoded"); } // store the metadata @@ -170,7 +169,7 @@ class Annotation implements Driver **Note:** this version of listener will support only ORM Entities -``` php +```php addEventSubscriber($encoderListener); ### Create an entity with some fields to encode -``` php +```php ## Setup and autoloading -Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup) -or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example) +Read the [documentation](./annotations.md#em-setup) +or check the [example code](../example) on how to setup and use the extensions in most optimized way. ## ReferenceIntegrity Document example: -``` php +```php - -## Yaml mapping example: - -Yaml mapped Article: **/mapping/yaml/Documents.Article.dcm.yml** - -``` ---- -Document\Type: - type: document - collection: types - fields: - id: - id: true - title: - type: string - article: - reference: true - type: one - mappedBy: type - targetDocument: Document\Article - gedmo: - referenceIntegrity: nullify # or pull or restrict - -``` - It is necessary to have the 'mappedBy' option set, to be able to access the referenced documents. @@ -114,7 +87,7 @@ It is necessary to have the 'mappedBy' option set, to be able to access the refe Few operations to see 'nullify' in action: -``` php +```php setTitle('My Article'); @@ -138,7 +111,7 @@ $article->getType(); // won't be referenced to Type anymore Few operations to see 'pull' in action: -``` php +```php setTitle('My Article'); diff --git a/doc/references.md b/doc/references.md index 20de858a0e..75d8f925e3 100644 --- a/doc/references.md +++ b/doc/references.md @@ -1,4 +1,4 @@ -# Cross Object Mapper References behavior extension for Doctrine 2 +# Cross Object Mapper References behavior extension for Doctrine Create documents and entities that contain references to each other. @@ -19,61 +19,68 @@ The following options are possible on reference one and many associations: - **class** - The associated class name. - **mappedBy** - The property name for the owning side of this association. -## Annotations +## Annotations and attributes -**@Gedmo\ReferenceMany** +**Gedmo\ReferenceMany** -``` php +```php -## Sluggable Entity example: +## Sluggable mapping: ### Sluggable annotations: -- **@Gedmo\Mapping\Annotation\Slug** it will use this column to store **slug** generated -**fields** option must be specified, an array of field names to slug +- **@Gedmo\Mapping\Annotation\Slug** it will to store in this property the **slug** generated. +**fields** option must be specified as an array of field names that will be used for generate the slug. + +### Sluggable attributes: + +- **\#[Gedmo\Mapping\Annotation\Slug]** it will to store in this property the **slug** generated. +**fields** option must be specified as an array of field names that will be used for generate the slug. + +**Note:** the examples shown here are using annotations and attributes for mapping, you should use +one of them, not both. **Note:** that Sluggable interface is not necessary, except in cases there you need to identify entity as being Sluggable. The metadata is loaded only once then cache is activated -**Note:** 2.0.x version of extensions used @Gedmo\Mapping\Annotation\Sluggable to identify -the field for slug - -``` php +```php - -## Yaml mapping example - -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** - -``` ---- -Entity\Article: - type: entity - table: articles - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - code: - type: string - length: 16 - slug: - type: string - length: 128 - gedmo: - slug: - separator: _ - style: camel - fields: - - title - - code - indexes: - search_idx: - columns: slug -``` - ## Xml mapping example **Note:** xml driver is not yet adapted for single slug mapping -``` xml +```xml @@ -314,7 +255,7 @@ Entity\Article: ### To save **Article** and generate slug simply use: -``` php +```php setTitle('the title'); @@ -326,40 +267,58 @@ echo $article->getSlug(); // prints: the-title-my-code ``` -### Some other configuration options for **slug** annotation: +### Some other configuration options for **slug** annotation and attribute: - **fields** (required, default=[]) - list of fields for slug - **updatable** (optional, default=true) - **true** to update the slug on sluggable field changes, **false** - otherwise -- **unique** (optional, default=true) - **true** if slug should be unique and if identical it will be prefixed, **false** - otherwise +- **unique** (optional, default=true) - **true** if slug should be unique and if identical it will be suffixed, **false** - otherwise - **unique_base** (optional, default=null) - used in conjunction with **unique**. The name of the entity property that should be used as a key when doing a uniqueness check. - **separator** (optional, default="-") - separator which will separate words in slug - **prefix** (optional, default="") - prefix which will be added to the generated slug - **suffix** (optional, default="") - suffix which will be added to the generated slug - **style** (optional, default="default") - **"default"** all letters will be lowercase, **"camel"** - first word letter will be uppercase, **"upper"**- all word letter will be uppercase and **"lower"**- all word letter will be lowercase -- **handlers** (optional, default=[]) - list of slug handlers, like tree path slug, or customized, for example see bellow +- **handlers** (only available in annotations, optional, default=[]) - list of slug handlers, like tree path slug, or customized, for example see bellow +- **uniqueOverTranslations** (optional, default=false) - **true** if slug should be unique over translations and if identical it will be suffixed, **false** - otherwise **Note**: handlers are totally optional +When using attributes, SlugHandlers are defined directly at property level and their options are passed as an array +instead of `SlugHandlerOption`. + **TreeSlugHandler** -``` php +```php 'parent', + 'separator' => '/', +])] +#[Doctrine\ORM\Mapping\Column(length: 64, unique: true)] private $slug; ``` **RelativeSlugHandler**: -``` php +```php 'category', + 'relationSlugField' => 'slug', + 'separator' => '/', +])] +#[Doctrine\ORM\Mapping\Column(length: 64, unique: true)]` private $slug; ``` If the relationSlugField you are using is not a slug field but a string field for example you can make sure the relationSlugField is also urilized with: -``` php +```php 'category', + 'relationSlugField' => 'title', + 'separator' => '/', + 'urilize' => true, +])] +#[Doctrine\ORM\Mapping\Column(length: 64, unique: true)] private $slug; ``` @@ -402,27 +380,43 @@ This will make sure that the 'title' field in the category entity is url friendl **InversedRelativeSlugHandler** -``` php +```php Person::class, + 'mappedBy' => 'category', + 'inverseSlugField' => 'slug', +])] +#[Doctrine\ORM\Mapping\Column(length: 64, unique: true)] private $slug; ``` ### Example -``` php +```php setTitle('the title'); @@ -472,10 +477,12 @@ echo $article->getSlug(); To set your own custom transliterator, which would be used to generate the slug, use: -``` php +```php setTransliterator($callable); // or use a closure @@ -497,7 +504,7 @@ In case if you want the slug to regenerate itself based on sluggable fields, set *Note: in previous versions empty strings would also cause the slug to be regenerated. This behaviour was changed in v2.3.8.* -``` php +```php find('Entity\Something', $id); $entity->setSlug(null); @@ -511,7 +518,7 @@ $em->flush(); Sometimes you might need to set it manually, etc if generated one does not look satisfying enough. Sluggable will ensure uniqueness of the slug. -``` php +```php setSluggableField('won\'t be taken into account'); @@ -529,7 +536,7 @@ If you want to attach **TranslatableListener** also add it to EventManager after the **SluggableListener**. It is important because slug must be generated first before the creation of it`s translation. -``` php +```php addEventSubscriber($translatableListener); And the Entity should look like: -``` php +```php +`uniqueOverTranslations` option is used to ensure that the slug is unique for each translated slugs. + ## Using slug handlers: There are built-in slug handlers like described in configuration options of slug, but there can be also customized slug handlers depending on use cases. Usually the most logic use case -is for related slug. For instance if user has a **ManyToOne relation to a **Company** we -would like to have a url like **http://example.com/knplabs/gedi where **KnpLabs** -is a company and user name is **Gedi**. In this case relation has a path separator **/** +is for related slug. For instance if user has a **ManyToOne** relation to a **Company** we +would like to have a url like `http://example.com/knplabs/gedi` where **KnpLabs** +is a company and user name is **Gedi**. In this case relation has a path separator **/**. User entity example: -``` php +```php setTitle('KnpLabs'); diff --git a/doc/soft-deleteable.md b/doc/soft-deleteable.md new file mode 100644 index 0000000000..bd8ee0bafb --- /dev/null +++ b/doc/soft-deleteable.md @@ -0,0 +1,301 @@ +# Soft Deleteable Behavior Extension for Doctrine + +The **Soft Deleteable** behavior allows you to "soft delete" objects by marking them as deleted with a timestamp instead +of removing them from the database. + +## Index + +- [Getting Started](#getting-started) +- [Configuring Soft Deleteable Objects](#configuring-soft-deleteable-objects) +- [Using Traits](#using-traits) +- [Working with Filters](#working-with-filters) +- [Bulk Delete Support](#bulk-delete-support) +- [Time-Aware Soft Deletion](#time-aware-soft-deletion) +- ["Hard Delete" Soft Deleted Records](#hard-delete-soft-deleted-records) + +## Getting Started + +The soft deleteable behavior can be added to a supported Doctrine object manager by registering its event subscriber +when creating the manager. + +```php +use Gedmo\SoftDeleteable\SoftDeleteableListener; + +$listener = new SoftDeleteableListener(); + +// The $om is either an instance of the ORM's entity manager or the MongoDB ODM's document manager +$om->getEventManager()->addEventSubscriber($listener); +``` + +### Removing Soft-Deleted Entities from the Identity Map + +When an entity is soft-deleted, it remains managed by Doctrine and can still be queried by its primary key as it stays +in the identity map. +To automatically remove such entities after a flush, enable the `$handlePostFlushEvent` option: + +```php +$listener = new SoftDeleteableListener(true); +``` + +This detaches soft-deleted entities from the identity map but may cause issues if a deleted entity is still referenced +by another managed entity with cascade persist, as later `flush()` calls may treat it as new and attempt to re-insert +it. + +Enable this option only if you need deleted entities to be fully detached after flushing. + +### Configuring Filters + +To automatically filter out soft-deleted records from all queries, you need to register and enable the appropriate filter for your object manager. + +#### For Doctrine ORM + +```php +use Doctrine\ORM\Configuration; +use Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter; + +// Register the filter during configuration +$config = new Configuration(); +$config->addFilter('soft-deleteable', SoftDeleteableFilter::class); + +// Enable the filter (usually in your application bootstrap) +$em->getFilters()->enable('soft-deleteable'); +``` + +#### For MongoDB ODM + +```php +use Doctrine\ODM\MongoDB\Configuration; +use Gedmo\SoftDeleteable\Filter\ODM\SoftDeleteableFilter; + +// Register the filter during configuration +$config = new Configuration(); +$config->addFilter('soft-deleteable', SoftDeleteableFilter::class); + +// Enable the filter (usually in your application bootstrap) +$dm->getFilterCollection()->enable('soft-deleteable'); +``` + +## Configuring Soft Deleteable Objects + +The soft deleteable extension can be configured with [annotations](./annotations.md#soft-deleteable-extension), +[attributes](./attributes.md#soft-deleteable-extension), or XML configuration (matching the mapping of +your domain models). The full configuration for annotations and attributes can be reviewed in +the linked documentation. + +The below examples show the basic configuration for the extension, marking a class as soft deleteable. + +### Attribute Configuration + +```php + + + + + + + + + + + + + + +``` + +### Annotation Configuration + +> [!NOTE] +> Support for annotations is deprecated and will be removed in 4.0. + +```php +getFilters()->enable('soft-deleteable'); + +// Disable the filter to show all records, including soft-deleted ones +$em->getFilters()->disable('soft-deleteable'); + +// Check if the filter is enabled +$isEnabled = $em->getFilters()->isEnabled('soft-deleteable'); +``` + +### Per-Object Filter Control + +You can enable or disable the filter for specific object types using the enable and disable methods on the filter classes. +For example, when using the ORM: + +```php +// Get the filter instance +$filter = $em->getFilters()->enable('soft-deleteable'); + +// Disable filtering for a specific entity (show all records, including soft-deleted) +$filter->disableForEntity(Article::class); + +// Re-enable filtering for a specific entity +$filter->enableForEntity(Article::class); +``` + +For MongoDB ODM users, replace "Entity" with "Document" in the method names (i.e. `enableForDocument` and `disableForDocument`). + +## Bulk DELETE Support + +> [!NOTE] +> This feature is only available with the ORM. + +The soft deleteable extension includes a query walker that automatically converts DQL DELETE statements into UPDATE +statements that set the deletion timestamp, allowing you to perform bulk soft-deletion operations. + +### Using the Query Walker + +To use DQL DELETE queries with soft deleteable entities, you need to specify the `SoftDeleteableWalker` as a custom output walker: + +```php +use Doctrine\ORM\Query; +use Gedmo\SoftDeleteable\Query\TreeWalker\SoftDeleteableWalker; + +// Create a DQL DELETE query +$query = $em->createQuery('DELETE FROM App\Entity\Article a WHERE a.category = :category'); +$query->setParameter('category', $category); + +// Set the query walker to convert the DELETE query to UPDATE +$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, SoftDeleteableWalker::class); + +// Execute the query +$query->execute(); +``` + +## Time-Aware Soft Deletion + +The soft deleteable extension supports "time-aware" deletion, where you can schedule objects for deletion at a future time. + +### Enabling Time-Aware Support + +```php +#[ORM\Entity] +#[Gedmo\SoftDeleteable(timeAware: true)] +class Article +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + public ?int $id = null; + + #[ORM\Column(type: Types::STRING)] + public ?string $title = null; + + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + public ?\DateTimeImmutable $deletedAt = null; +} +``` + +### Usage Example + +```php +// Schedule an article for deletion in the future +$article = new Article(); +$article->setTitle('Scheduled for deletion'); +$article->setDeletedAt(new \DateTimeImmutable('+1 week')); // Delete in 1 week +$em->persist($article); +$em->flush(); + +// The article will be visible now (deletion time hasn't passed) +$found = $em->getRepository(Article::class)->findOneBy(['title' => 'Scheduled for deletion']); +assert($found !== null); // Found because deletion time is in the future + +// After the scheduled time passes, the article will be automatically filtered out +// (without needing to run any cleanup processes) +``` + +## "Hard Delete" Soft Deleted Records + +By default, the soft deleteable extension allows soft deleted records to be "hard deleted" (fully removed from the database) +by deleting them a second time. However, by setting the `hardDelete` parameter in the configuration to `false`, you can +prevent soft deleted records from being deleted at all. diff --git a/doc/softdeleteable.md b/doc/softdeleteable.md deleted file mode 100644 index 35fbcac0a3..0000000000 --- a/doc/softdeleteable.md +++ /dev/null @@ -1,282 +0,0 @@ -# SoftDeleteable behavior extension for Doctrine 2 - -**SoftDeleteable** behavior allows to "soft delete" objects, filtering them -at SELECT time by marking them as with a timestamp, but not explicitly removing them from the database. - -Features: - -- Works with DQL DELETE queries (using a Query Hint). -- All SELECT queries will be filtered, not matter from where they are executed (Repositories, DQL SELECT queries, etc). -- For now, it works only with the ORM -- Can be nested with other behaviors -- Annotation, Yaml and Xml mapping support for extensions -- Support for 'timeAware' option: When creating an entity set a date of deletion in the future and never worry about cleaning up at expiration time. - -Content: - -- [Including](#including-extension) the extension -- Entity [example](#entity-mapping) -- [Yaml](#yaml-mapping) mapping example -- [Xml](#xml-mapping) mapping example -- Usage [examples](#usage) -- Using [Traits](#traits) - - - -## Setup and autoloading - -Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup) -or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example) -on how to setup and use the extensions in most optimized way. - -With SoftDeleteable there's one more step you need to do. You need to add the filter to your configuration: - -``` php - -$config = new Doctrine\ORM\Configuration; - -// Your configs.. - -$config->addFilter('soft-deleteable', 'Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter'); -``` - -And then you can access the filter from your EntityManager to enable or disable it with the following code: - -``` php -// This will enable the SoftDeleteable filter, so entities which were "soft-deleted" will not appear -// in results -// You should adapt the filter name to your configuration (ex: softdeleteable) -$em->getFilters()->enable('soft-deleteable'); - -// This will disable the SoftDeleteable filter, so entities which were "soft-deleted" will appear in results -$em->getFilters()->disable('soft-deleteable'); -``` - -Or from your DocumentManager (ODM): - -``` php -// This will enable the SoftDeleteable filter, so entities which were "soft-deleted" will not appear -// in results -// You should adapt the filter name to your configuration (ex: softdeleteable) -$em->getFilterCollection()->enable('soft-deleteable'); - -// This will disable the SoftDeleteable filter, so entities which were "soft-deleted" will appear in results -$em->getFilterCollection()->disable('soft-deleteable'); -``` - -**NOTE:** by default all filters are disabled, so you must explicitly enable **soft-deleteable** filter in your setup -or whenever you need it. - - - -## SoftDeleteable Entity example: - -### SoftDeleteable annotations: -- **@Gedmo\Mapping\Annotation\SoftDeleteable** this class annotation tells if a class is SoftDeleteable. It has a -mandatory parameter "fieldName", which is the name of the field to be used to hold the known "deletedAt" field. It -must be of any of the date types. - -Available configuration options: -- **fieldName** - The name of the field that will be used to determine if the object is removed or not (NULL means -it's not removed. A date value means it was removed). NOTE: The field MUST be nullable. - -**Note:** that SoftDeleteable interface is not necessary, except in cases where -you need to identify entity as being SoftDeleteable. The metadata is loaded only once then -cache is activated. - -``` php -id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function getDeletedAt() - { - return $this->deletedAt; - } - - public function setDeletedAt($deletedAt) - { - $this->deletedAt = $deletedAt; - } -} -``` - - - -## Yaml mapping example: - -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** - -``` ---- -Entity\Article: - type: entity - table: articles - gedmo: - soft_deleteable: - field_name: deletedAt - time_aware: false - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - deletedAt: - type: date - nullable: true -``` - - - -## Xml mapping example - -``` xml - - - - - - - - - - - - - - - - -``` - - - -## Usage: - -``` php -setTitle('My Article'); - -$em->persist($article); -$em->flush(); - -// Now if we remove it, it will set the deletedAt field to the actual date -$em->remove($article); -$em->flush(); - -$repo = $em->getRepository('Article'); -$art = $repo->findOneBy(array('title' => 'My Article')); - -// It should NOT return the article now -$this->assertNull($art); - -// But if we disable the filter, the article should appear now -$em->getFilters()->disable('soft-deleteable'); - -$art = $repo->findOneBy(array('title' => 'My Article')); - -$this->assertTrue(is_object($art)); - -// Enable / Disable filter filter, for specified entity (default is enabled for all) -$filter = $em->getFilters()->enable('soft-deleteable'); -$filter->disableForEntity('Entity\Article'); -$filter->enableForEntity('Entity\Article'); - -// Undelete the entity by setting the deletedAt field to null -$article->setDeletedAt(null); -``` - -Easy like that, any suggestions on improvements are very welcome. - - - -## Traits - -You can use softDeleteable traits for quick **deletedAt** timestamp definitions -when using annotation mapping. -There is also a trait without annotations for easy integration purposes. - -**Note:** this feature is only available since php **5.4.0**. And you are not required -to use the Traits provided by extensions. - -``` php - +Contents: +- [Setup and autoloading](#setup-and-autoloading) +- [Sortable mapping](#sortable-mapping) + - [Annotations](#annotation-mapping-example) + - [Attributes](#attribute-mapping-example) + - [Xml](#xml-mapping-example) +- [Basic usage examples](#basic-usage-examples) +- [Custom comparison method](#custom-comparison) ## Setup and autoloading - -Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup) -or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example) +Read the [documentation](./annotations.md#em-setup) or check the [example code](../example) on how to setup and use the extensions in most optimized way. - +## Sortable mapping +- [SortableGroup](../src/Mapping/Annotation/SortableGroup.php) - used to specify property for **grouping** +- [SortablePosition](../src/Mapping/Annotation/SortablePosition.php) - used to specify property to store **position** index -## Sortable Entity example: +| | SortableGroup | SortablePosition | +|-------------|---------------------------------------------|------------------------------------------------| +| Annotations | `@Gedmo\Mapping\Annotation\SortableGroup` | `@Gedmo\Mapping\Annotation\SortablePosition` | +| Attributes | `#[Gedmo\Mapping\Annotation\SortableGroup]` | `#[Gedmo\Mapping\Annotation\SortablePosition]` | +| Xml | `` | `` | -### Sortable annotations: +> Implementing **[Sortable interface](../src/Sortable/Sortable.php) is optional**, except in cases there you need to identify entity as being Sortable. +> The metadata is loaded only once then cache is activated. -- **@Gedmo\Mapping\Annotation\SortableGroup** it will use this field for **grouping** -- **@Gedmo\Mapping\Annotation\SortablePosition** it will use this column to store **position** index - -**Note:** that Sortable interface is not necessary, except in cases there -you need to identify entity as being Sortable. The metadata is loaded only once then -cache is activated - -**Note:** that you should register SortableRepository (or a subclass) as the repository in the Entity +> You **should register [SortableRepository](../src/Sortable/Entity/Repository/SortableRepository.php)** (or a subclass) as the repository in the Entity annotation to benefit from its query methods. -``` php +### Annotation mapping example + +```php +### Attribute mapping example -## Yaml mapping example +```php +id; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } + + public function setPosition(int $position): void + { + $this->position = $position; + } + + public function getPosition(): int + { + return $this->position; + } + + public function setCategory(string $category): void + { + $this->category = $category; + } - + public function getCategory(): string + { + return $this->category; + } +} +``` -## Xml mapping example +### Xml mapping example -``` xml +```xml @@ -183,13 +202,11 @@ Entity\Item: ``` - - -## Basic usage examples: +## Basic usage examples ### To save **Items** at the end of the sorting list simply do: -``` php +```php getPosition(); // prints: 1 ``` -### Save **Item** at a given position: +### Save **Item** at a given position -``` php +```php setName('item 1'); @@ -243,9 +260,9 @@ foreach ($items as $item) { // 2: item 2 ``` -### Reordering the sorted list: +### Reordering the sorted list -``` php +```php setName('item 1'); @@ -289,9 +306,37 @@ If you want to use a foreign key / relation as sortable group, you have to put @ private $parent; ``` - To move an item at the end of the list, you can set the position to `-1`: ``` $item2->setPosition(-1); ``` + +## Custom comparison + +Sortable works by comparing objects in the same group to see how they should be positioned. From time to time you may want to customize the way these +objects are compared by simply implementing the Doctrine\Common\Comparable interface + +```php + - -## Symfony2 application - -First of all, we will need a symfony2 startup application, let's say [symfony-standard edition -with composer](http://github.com/KnpLabs/symfony-with-composer). Follow the standard setup: - -- `git clone git://github.com/KnpLabs/symfony-with-composer.git example` -- `cd example && rm -rf .git && php bin/vendors install` -- ensure your application loads and meets requirements, by following the url: **http://your_virtual_host/app_dev.php** - -Now let's add the **gedmo/doctrine-extensions** into **composer.json** - -```json -{ - "require": { - "php": ">=5.3.2", - "symfony/symfony": ">=2.0.9,<2.1.0-dev", - "doctrine/orm": ">=2.1.0,<2.2.0-dev", - "twig/extensions": "*", - - "symfony/assetic-bundle": "*", - "sensio/generator-bundle": "2.0.*", - "sensio/framework-extra-bundle": "2.0.*", - "sensio/distribution-bundle": "2.0.*", - "jms/security-extra-bundle": "1.0.*", - "gedmo/doctrine-extensions": "dev-master" - }, - - "autoload": { - "psr-0": { - "Acme": "src/" - } - } -} -``` - -Update vendors, run: **php composer.phar update gedmo/doctrine-extensions** -Initially in this package you have **doctrine2 orm** included, so we will base our setup -and configuration for this specific connection. Do not forget to configure your database -connection parameters, edit **app/config/parameters.yml** - - - -## Mapping - -Let's start from the mapping. In case you use the **translatable**, **tree** or **loggable** -extension you will need to map those abstract mapped superclasses for your ORM to be aware of. -To do so, add some mapping info to your **doctrine.orm** configuration, edit **app/config/config.yml**: - -```yaml -doctrine: - dbal: -# your dbal config here - - orm: - auto_generate_proxy_classes: %kernel.debug% - auto_mapping: true -# only these lines are added additionally - mappings: - translatable: - type: annotation - alias: Gedmo - prefix: Gedmo\Translatable\Entity - # make sure vendor library location is correct - dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity" -``` - -After that, running **php app/console doctrine:mapping:info** you should see the output: - -``` -Found 3 entities mapped in entity manager default: -[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation -[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation -[OK] Gedmo\Translatable\Entity\Translation -``` -Well, we mapped only **translatable** for now, it really depends on your needs, which extensions -your application uses. - -**Note:** there is **Gedmo\Translatable\Entity\Translation** which is not a super class, in that case -if you create a doctrine schema, it will add **ext_translations** table, which might not be useful -to you also. To skip mapping of these entities, you can map **only superclasses** - -```yaml -mappings: - translatable: - type: annotation - alias: Gedmo - prefix: Gedmo\Translatable\Entity - # make sure vendor library location is correct - dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity/MappedSuperclass" -``` - -The configuration above, adds a **/MappedSuperclass** into directory depth, after running -**php app/console doctrine:mapping:info** you should only see now: - -``` -Found 2 entities mapped in entity manager default: -[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation -[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation -``` - -This is very useful for advanced requirements and quite simple to understand. So now let's map -everything the extensions provide: - -```yaml -# only orm config branch of doctrine -orm: - auto_generate_proxy_classes: %kernel.debug% - auto_mapping: true -# only these lines are added additionally - mappings: - translatable: - type: annotation - alias: Gedmo - prefix: Gedmo\Translatable\Entity - # make sure vendor library location is correct - dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity" - loggable: - type: annotation - alias: Gedmo - prefix: Gedmo\Loggable\Entity - dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity" - tree: - type: annotation - alias: Gedmo - prefix: Gedmo\Tree\Entity - dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Tree/Entity" -``` - - - -## Doctrine extension listener services - -Next, the heart of extensions are behavioral listeners which pours all the sugar. We will -create a **yml** service file in our config directory. The setup can be different, your config could be located -in the bundle, it depends on your preferences. Edit **app/config/doctrine_extensions.yml** - -```yaml -# services to handle doctrine extensions -# import it in config.yml -services: - # KernelRequest listener - extension.listener: - class: Acme\DemoBundle\Listener\DoctrineExtensionListener - calls: - - [ setContainer, [ @service_container ] ] - tags: - # translatable sets locale after router processing - - { name: kernel.event_listener, event: kernel.request, method: onLateKernelRequest, priority: -10 } - # loggable hooks user username if one is in security context - - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } - # translatable sets locale such as default application locale before command execute - - { name: kernel.event_listener, event: console.command, method: onConsoleCommand, priority: -10 } - - - # Doctrine Extension listeners to handle behaviors - gedmo.listener.tree: - class: Gedmo\Tree\TreeListener - tags: - - { name: doctrine.event_subscriber, connection: default } - calls: - - [ setAnnotationReader, [ "@annotation_reader" ] ] - - gedmo.listener.translatable: - class: Gedmo\Translatable\TranslatableListener - tags: - - { name: doctrine.event_subscriber, connection: default } - calls: - - [ setAnnotationReader, [ "@annotation_reader" ] ] - - [ setDefaultLocale, [ %locale% ] ] - - [ setTranslationFallback, [ false ] ] - - gedmo.listener.timestampable: - class: Gedmo\Timestampable\TimestampableListener - tags: - - { name: doctrine.event_subscriber, connection: default } - calls: - - [ setAnnotationReader, [ "@annotation_reader" ] ] - - gedmo.listener.sluggable: - class: Gedmo\Sluggable\SluggableListener - tags: - - { name: doctrine.event_subscriber, connection: default } - calls: - - [ setAnnotationReader, [ "@annotation_reader" ] ] - - gedmo.listener.sortable: - class: Gedmo\Sortable\SortableListener - tags: - - { name: doctrine.event_subscriber, connection: default } - calls: - - [ setAnnotationReader, [ "@annotation_reader" ] ] - - gedmo.listener.loggable: - class: Gedmo\Loggable\LoggableListener - tags: - - { name: doctrine.event_subscriber, connection: default } - calls: - - [ setAnnotationReader, [ "@annotation_reader" ] ] -``` - -So what does it include in general? Well, it creates services for all extension listeners. -You can remove some which you do not use, or change them as you need. **Translatable** for instance, -sets the default locale to the value of your `%locale%` parameter, you can configure it differently. - -**Note:** In case you noticed, there is **Acme\DemoBundle\Listener\DoctrineExtensionListener**. -You will need to create this listener class if you use **loggable** or **translatable** -behaviors. This listener will set the **locale used** from request and **username** to -loggable. So, to finish the setup create **Acme\DemoBundle\Listener\DoctrineExtensionListener** - -```php -container = $container; - } - - public function onLateKernelRequest(GetResponseEvent $event) - { - $translatable = $this->container->get('gedmo.listener.translatable'); - $translatable->setTranslatableLocale($event->getRequest()->getLocale()); - } - - public function onConsoleCommand() - { - $this->container->get('gedmo.listener.translatable') - ->setTranslatableLocale($this->container->get('translator')->getLocale()); - } - - public function onKernelRequest(GetResponseEvent $event) - { - $securityContext = $this->container->get('security.context', ContainerInterface::NULL_ON_INVALID_REFERENCE); - if (null !== $securityContext && null !== $securityContext->getToken() && $securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) { - $loggable = $this->container->get('gedmo.listener.loggable'); - $loggable->setUsername($securityContext->getToken()->getUsername()); - } - } -} -``` -Do not forget to import **doctrine_extensions.yml** in your **app/config/config.yml**: - -```yaml -# file: app/config/config.yml -imports: - - { resource: parameters.yml } - - { resource: security.yml } - - { resource: doctrine_extensions.yml } - -# ... configuration follows -``` - - - -## Example - -After that, you have your extensions set up and ready to be used! Too easy right? Well, -if you do not believe me, let's create a simple entity in our **Acme** project: - -```php -id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function getCreated() - { - return $this->created; - } - - public function getUpdated() - { - return $this->updated; - } -} -``` - -Now, let's have some fun: - -- if you have not created the database yet, run `php app/console doctrine:database:create` -- create the schema `php app/console doctrine:schema:create` - -Everything will work just fine, you can modify the **Acme\DemoBundle\Controller\DemoController** -and add an action to test how it works: - -```php -// file: src/Acme/DemoBundle/Controller/DemoController.php -// include this code portion - -/** - * @Route("/posts", name="_demo_posts") - */ -public function postsAction() -{ - $em = $this->getDoctrine()->getEntityManager(); - $repository = $em->getRepository('AcmeDemoBundle:BlogPost'); - // create some posts in case if there aren't any - if (!$repository->findOneById('hello_world')) { - $post = new \Acme\DemoBundle\Entity\BlogPost(); - $post->setTitle('Hello world'); - - $next = new \Acme\DemoBundle\Entity\BlogPost(); - $next->setTitle('Doctrine extensions'); - - $em->persist($post); - $em->persist($next); - $em->flush(); - } - $posts = $em - ->createQuery('SELECT p FROM AcmeDemoBundle:BlogPost p') - ->getArrayResult() - ; - die(var_dump($posts)); -} -``` - -Now if you follow the url: **http://your_virtual_host/app_dev.php/demo/posts** you -should see a print of posts, this is only an extension demo, we will not create a template. - - - -## More tips - -Regarding, the setup, I do not think it's too complicated to use, in general it is simple -enough, and lets you understand at least small parts on how you can hook mappings into doctrine, and -how easily extension services are added. This configuration does not hide anything behind -curtains and allows you to modify the configuration as you require. - -### Multiple entity managers - -If you use more than one entity manager, you can simply tag the listener -with other the manager name: - -```yaml -services: - # tree behavior - gedmo.listener.tree: - class: Gedmo\Tree\TreeListener - tags: - - { name: doctrine.event_subscriber, connection: default } - # additional ORM subscriber - - { name: doctrine.event_subscriber, connection: other_connection } - # ODM MongoDb subscriber, where **default** is manager name - - { name: doctrine_mongodb.odm.event_subscriber } - calls: - - [ setAnnotationReader, [ @annotation_reader ] ] -``` - -Regarding, mapping of ODM mongodb, it's basically the same: - -```yaml -doctrine_mongodb: - default_database: 'my_database' - default_connection: 'default' - default_document_manager: 'default' - connections: - default: ~ - document_managers: - default: - connection: 'default' - auto_mapping: true - mappings: - translatable: - type: annotation - alias: GedmoDocument - prefix: Gedmo\Translatable\Document - # make sure vendor library location is correct - dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Document" -``` - -This also shows, how to make mappings based on single manager. All what differs is that **Document** -instead of **Entity** is used. I haven't tested it with mongo though. - -**Note:** [extension repository](http://github.com/l3pp4rd/DoctrineExtensions) contains all -[documentation](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/doc) you may need -to understand how you can use it in your projects. - - - -## Alternative over configuration - -You can use [StofDoctrineExtensionsBundle](http://github.com/stof/StofDoctrineExtensionsBundle) which is a wrapper of these extensions - -## Troubleshooting - -- Make sure there are no *.orm.yml or *.orm.xml files for your Entities in your bundles Resources/config/doctrine directory. With those files in place the annotations won't be taken into account. diff --git a/doc/timestampable.md b/doc/timestampable.md index 5a223b7749..79b3638233 100644 --- a/doc/timestampable.md +++ b/doc/timestampable.md @@ -1,675 +1,273 @@ -# Timestampable behavior extension for Doctrine 2 +# Timestampable Behavior Extension for Doctrine -**Timestampable** behavior will automate the update of date fields -on your Entities or Documents. It works through annotations and can update -fields on creation, update, property subset update, or even on specific property value change. +The **Timestampable** behavior automates the update of timestamps on your Doctrine objects. -Features: +## Index -- Automatic predefined date field update on creation, update, property subset update, and even on record property changes -- ORM and ODM support using same listener -- Specific annotations for properties, and no interface required -- Can react to specific property or relation changes to specific value -- Can be nested with other behaviors -- Annotation, Yaml and Xml mapping support for extensions +- [Getting Started](#getting-started) +- [Configuring Timestampable Objects](#configuring-timestampable-objects) +- [Using Traits](#using-traits) +- [Logging Changes For Specific Actions](#logging-changes-for-specific-actions) -Update **2012-06-26** +## Getting Started -- Allow multiple values for on="change" +The timestampable behavior can be added to a supported Doctrine object manager by registering its event subscriber +when creating the manager. -Update **2012-03-10** +```php +use Gedmo\Timestampable\TimestampableListener; -- Add [Timestampable traits](#traits) +$listener = new TimestampableListener(); -Update **2011-04-04** - -- Made single listener, one instance can be used for any object manager -and any number of them - -**Note:** -- Last update date: **2012-01-02** - -**Portability:** - -- **Timestampable** is now available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle) -ported to **Symfony2** by **Christophe Coevoet**, together with all other extensions - -This article will cover the basic installation and functionality of **Timestampable** behavior - -Content: - -- [Including](#including-extension) the extension -- Entity [example](#entity-mapping) -- Document [example](#document-mapping) -- [Yaml](#yaml-mapping) mapping example -- [Xml](#xml-mapping) mapping example -- Advanced usage [examples](#advanced-examples) -- Using [Traits](#traits) - - +// The $om is either an instance of the ORM's entity manager or the MongoDB ODM's document manager +$om->getEventManager()->addEventSubscriber($listener); +``` -## Setup and autoloading +### Using a Clock -Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup) -or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example) -on how to setup and use the extensions in most optimized way. +The timestampable extension supports using a [PSR-20 Clock](https://www.php-fig.org/psr/psr-20/) as the provider for its +timestamps, falling back to creating a new `DateTime` instance when not available. - +To use a clock in the timestampable extension, you can provide one by calling the listener's `setClock` method. -## Timestampable Entity example: +```php +$listener->setClock($clock); +``` -### Timestampable annotations: -- **@Gedmo\Mapping\Annotation\Timestampable** this annotation tells that this column is timestampable -by default it updates this column on update. If column is not date, datetime or time -type it will trigger an exception. +## Configuring Timestampable Objects -Available configuration options: +The Itimestampable extension can be configured with [annotations](./annotations.md#timestampable-extension), +[attributes](./attributes.md#timestampable-extension), or XML configuration (matching the mapping of +your domain models). The full configuration for annotations and attributes can be reviewed in +the linked documentation. -- **on** - is main option and can be **create, update, change** this tells when it -should be updated -- **field** - only valid if **on="change"** is specified, tracks property or a list of properties for changes -- **value** - only valid if **on="change"** is specified and the tracked field is a single field (not an array), if the tracked field has this **value** +The below examples show the simplest and default configuration for the extension, setting a field +when the model is updated. -**Note:** that Timestampable interface is not necessary, except in cases where -you need to identify entity as being Timestampable. The metadata is loaded only once then -cache is activated +### Attribute Configuration -``` php +```php + - /** - * @var \DateTime $updated - * - * @Gedmo\Timestampable(on="update") - * @ORM\Column(type="datetime") - */ - private $updated; + + + + - /** - * @var \DateTime $contentChanged - * - * @ORM\Column(name="content_changed", type="datetime", nullable=true) - * @Gedmo\Timestampable(on="change", field={"title", "body"}) - */ - private $contentChanged; - - public function getId() - { - return $this->id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function setBody($body) - { - $this->body = $body; - } - - public function getBody() - { - return $this->body; - } - - public function getCreated() - { - return $this->created; - } - - public function getUpdated() - { - return $this->updated; - } - - public function getContentChanged() - { - return $this->contentChanged; - } -} + + + + + ``` - +### Annotation Configuration -## Timestampable Document example: +> [!NOTE] +> Support for annotations is deprecated and will be removed in 4.0. -``` php +```php id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function setBody($body) - { - $this->body = $body; - } - - public function getBody() - { - return $this->body; - } - - public function getCreated() - { - return $this->created; - } - - public function getUpdated() - { - return $this->updated; - } - - public function getContentChanged() - { - return $this->contentChanged; - } + public ?\DateTimeImmutable $updatedAt = null; } ``` -Now on update and creation these annotated fields will be automatically updated - - - -## Yaml mapping example: - -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** - -```yaml ---- -Entity\Article: - type: entity - table: articles - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - created: - type: date - gedmo: - timestampable: - on: create - updated: - type: datetime - gedmo: - timestampable: - on: update -``` - - - -## Xml mapping example +### Supported Field Types -``` xml - - +The timestampable extension supports the following field types for the timestamp field: - - - - +- Date (`date` and `date_immutable`) +- Time (`time` and `time_immutable`) +- Date/Time (`datetime` and `datetime_immutable`) +- Date/Time with timezone (`datetimetz` and `datetimetz_immutable`) +- Timestamp (`timestamp`) +- Variable Date/Time (`vardatetime`) (Supported by the ORM and DBAL only) +- Integer (`integer` only) - - - - - - - - - +## Using Traits - - - - +The timestampable extension provides traits which can be used to quickly add fields, and optionally the mapping configuration, +for a created at and updated at timestamp to be updated for the **create** and **update** actions. These traits are +provided as a convenience for a common configuration, for other use cases it is suggested you add your own fields and configurations. - -``` +- `Gedmo\Timestampable\Traits\Timestampable` adds a `$createdAt` and `$updatedAt` property, with getters and setters +- `Gedmo\Timestampable\Traits\TimestampableDocument` adds a `$createdAt` and `$updatedAt` property, with getters and setters + and mapping annotations and attributes for the MongoDB ODM +- `Gedmo\Timestampable\Traits\TimestampableEntity` adds a `$createdAt` and `$updatedAt` property, with getters and setters + and mapping annotations and attributes for the ORM - +## Logging Changes For Specific Actions -## Advanced examples: +In addition to supporting logging the timestamp for general create and update actions, the extension can also be configured to +log the timestamp for a change for specific fields or values. -### Using dependency of property changes +### Single Field Changed To Specific Value -Add another entity which would represent Article Type: +For example, we want to record the timestamp of when an article is published on a news site. To do this, we add a field to our object +and configure it using the **change** action, specifying the field and value we want it to match. -``` php +```php id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'published', value: true)] + public ?\DateTimeImmutable $updatedAt = null; } ``` -Now update the Article Entity to reflect published date on Type change: +The change action can also be configured to watch for changes on related objects using a dot notation path. In this example, +we log the timestamp for when the article was moved into an archived category. -``` php +```php type = $type; - } - - public function getId() - { - return $this->id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function getCreated() - { - return $this->created; - } - - public function getUpdated() - { - return $this->updated; - } - - public function getPublished() - { - return $this->published; - } + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'category.archived', value: true)] + public ?\DateTimeImmutable $updatedAt = null; } ``` -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** - -``` yaml ---- -Entity\Article: - type: entity - table: articles - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - created: - type: date - gedmo: - timestampable: - on: create - updated: - type: datetime - gedmo: - timestampable: - on: update - published: - type: datetime - gedmo: - timestampable: - on: change - field: type.title - value: Published - manyToOne: - type: - targetEntity: Entity\Type - inversedBy: articles -``` +### One of Many Fields Changed -Now few operations to get it all done: +The extension can also update a timestampable field when using the **change** action by specifying a list of fields to watch. +This also supports the dotted path notation, allowing you to watch changes on the model itself as well as related data. -``` php +```php setTitle('My Article'); - -$em->persist($article); -$em->flush(); -// article: $created, $updated were set - -$type = new Type; -$type->setTitle('Published'); - -$article = $em->getRepository('Entity\Article')->findByTitle('My Article'); -$article->setType($type); - -$em->persist($article); -$em->persist($type); -$em->flush(); -// article: $published, $updated were set +namespace App\Entity; -$article->getPublished()->format('Y-m-d'); // the date article type changed to published -``` - -Easy like that, any suggestions on improvements are very welcome - -### Creating a UTC DateTime type that stores your datetimes in UTC - -First, we define our custom data type (note the type name is datetime and the type extends DateTimeType which simply overrides the default Doctrine type): - -``` php -setTimeZone(self::$utc); + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + public ?int $id = null; - return $value->format($platform->getDateTimeFormatString()); - } + #[ORM\ManyToOne(targetEntity: Category::class)] + public ?Category $category = null; - public function convertToPHPValue($value, AbstractPlatform $platform) - { - if ($value === null) { - return null; - } + #[ORM\Column(type: Types::STRING, nullable: true)] + public ?string $metaDescription = null; - if (is_null(self::$utc)) { - self::$utc = new \DateTimeZone('UTC'); - } + #[ORM\Column(type: Types::STRING, nullable: true)] + public ?string $metaKeywords = null; - $val = \DateTime::createFromFormat($platform->getDateTimeFormatString(), $value, self::$utc); - - if (!$val) { - throw ConversionException::conversionFailed($value, $this->getName()); - } - - return $val; - } -} -``` - -Now in Symfony2, we register and override the **datetime** type. **WARNING:** this will override the **datetime** type for all your entities and for all entities in external bundles or extensions, so if you have some entities that require the standard **datetime** type from Doctrine, you must modify the above type and use a different name (such as **utcdatetime**). Additionally, you'll need to modify **Timestampable** so that it includes **utcdatetime** as a valid type. - -``` yaml -doctrine: - dbal: - types: - datetime: Acme\DoctrineExtensions\DBAL\Types\UTCDateTimeType -``` - -And our Entity properties look as expected: - -``` php - - -## Traits - -You can use timestampable traits for quick **createdAt** **updatedAt** timestamp definitions -when using annotation mapping. -There is also a trait without annotations for easy integration purposes. - -**Note:** this feature is only available since php **5.4.0**. And you are not required -to use the Traits provided by extensions. - -``` php - ## Translatable Entity example: @@ -101,7 +62,9 @@ used to override the global locale you need to identify an entity as being Translatable. The metadata is loaded only once when cache is activated -``` php +### Annotations + +```php id; + } + + public function setTitle($title) + { + $this->title = $title; + } + + public function getTitle() + { + return $this->title; + } + + public function setContent($content) + { + $this->content = $content; + } + + public function getContent() + { + return $this->content; + } + + public function setTranslatableLocale($locale) + { + $this->locale = $locale; + } +} +``` + ## Translatable Document example: -``` php +```php - -## Yaml mapping example - -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** - -``` ---- -Entity\Article: - type: entity - table: articles - gedmo: - translation: - locale: localeField -# using specific personal translation class: -# entity: Translatable\Fixture\CategoryTranslation - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - gedmo: - - translatable - content: - type: text - gedmo: - - translatable -``` - ## Xml mapping example -``` xml +```xml @@ -309,7 +308,7 @@ Entity\Article: Currently a global locale used for translations is "en_us" which was set in **TranslationListener** globally. To save article with its translations: -``` php +```php setTitle('my title in en'); @@ -324,7 +323,7 @@ matches current locale - it uses original record value as translation Now lets update our article in different locale: -``` php +```php find('Entity\Article', 1 /*article id*/); @@ -338,7 +337,7 @@ $em->flush(); This updated an article and inserted the translations for it in "de_de" locale To see and load all translations of **Translatable** Entity: -``` php +```php find('Entity\Article', 1 /*article id*/); @@ -367,7 +366,7 @@ Array ( As far as our global locale is now "en_us" and updated article has "de_de" values. Lets try to load it and it should be translated in English -``` php +```php getRepository('Entity\Article')->find(1/* id of article */); echo $article->getTitle(); @@ -391,7 +390,7 @@ the slug, so the value as an additional translation should be processed when cre ### Example of multiple translations: -``` php +```php getRepository('Gedmo\\Translatable\\Entity\\Translation'); @@ -445,7 +444,7 @@ do not have a translation in currently used locale. Now enough talking, here is an example: -``` php +```php getArrayResult(); // array hydration And even a subselect: -``` php +```php setHint( **NOTE:** if you use memcache or apc. You should set locale and other options like fallbacks to query through hints. Otherwise the query will be cached with a first used locale -``` php +```php setHint( @@ -507,6 +506,11 @@ $query->setHint( \Gedmo\Translatable\TranslatableListener::HINT_FALLBACK, 1 // fallback to default values in case if record is not translated ); +// refresh entities +$query->setHint( + \Doctrine\ORM\Query::HINT_REFRESH, + true // update entity with correct locale if it was already loaded before +); $articles = $query->getResult(); // object hydration ``` @@ -532,7 +536,7 @@ In case if **translation query walker** is used, you can additionally override: ### Overriding translation fallback -``` php +```php setHint(\Gedmo\Translatable\TranslatableListener::HINT_FALLBACK, 1); ``` @@ -540,7 +544,7 @@ $query->setHint(\Gedmo\Translatable\TranslatableListener::HINT_FALLBACK, 1); will fallback to default locale translations instead of empty values if used. And will override the translation listener setting for fallback. -``` php +```php setHint(\Gedmo\Translatable\TranslatableListener::HINT_FALLBACK, 0); ``` @@ -549,7 +553,7 @@ will do the opposite. ### Using inner join strategy -``` php +```php setHint(\Gedmo\Translatable\TranslatableListener::HINT_INNER_JOIN, true); ``` @@ -560,7 +564,7 @@ records in your result set for instance. ### Overriding translatable locale -``` php +```php setHint(\Gedmo\Translatable\TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'en'); ``` @@ -584,14 +588,14 @@ will fill untranslated values as blanks To set the default locale: -``` php +```php setDefaultLocale('en_us'); ``` To set translation fallback: -``` php +```php setTranslationFallback(true); // default is false ``` @@ -602,7 +606,7 @@ will not store extra record in translation table by default. If you need to store translation in default locale, set: -``` php +```php setPersistDefaultLocaleTranslation(true); // default is false ``` @@ -610,6 +614,15 @@ $translatableListener->setPersistDefaultLocaleTranslation(true); // default is f This would always store translations in all locales, also keeping original record translated field values in default locale set. +To set a default translation value upon a missing translation: + +``` php +setDefaultTranslationValue(''); // default is null +``` + +**Note**: By default the value is null, but it may cause a Type error for non-nullable getter upon a missing translation. + ### Translation Entity In some cases if there are thousands of records or even more.. we would like to @@ -619,19 +632,26 @@ your translations by extending the mapped superclass. ArticleTranslation Entity: -``` php +**Note:** this example is using annotations and attributes for mapping, you should use +one of them, not both. + +```php 'title', //you need to provide which field you wish to translate 'personal_translation' => 'ExampleBundle\Entity\Translation\ProductTranslation', //the personal translation entity - ``` -### Translations field type using Personal Translations with Symfony2: +### Translations field type using Personal Translations with Symfony: You can use [A2lixTranslationFormBundle](https://github.com/a2lix/TranslationFormBundle) to facilitate your translations. diff --git a/doc/tree.md b/doc/tree.md index f1e97de033..a64d4350a9 100644 --- a/doc/tree.md +++ b/doc/tree.md @@ -1,4 +1,4 @@ -# Tree - Nestedset behavior extension for Doctrine 2 +# Tree - Nestedset behavior extension for Doctrine **Tree** nested behavior will implement the standard Nested-Set behavior on your Entity. Tree supports different strategies. Currently it supports @@ -14,57 +14,20 @@ Features: - Synchronization of left, right values is automatic - Can support concurrent flush with many objects being persisted and updated - Can be nested with other extensions -- Annotation, Yaml and Xml mapping support for extensions +- Attribute, Annotation and Xml mapping support for extensions Thanks for contributions to: -- **[comfortablynumb](http://github.com/comfortablynumb) Gustavo Falco** for Closure and Materialized Path strategy -- **[everzet](http://github.com/everzet) Kudryashov Konstantin** for TreeLevel implementation -- **[stof](http://github.com/stof) Christophe Coevoet** for getTreeLeafs function - -Update **2012-06-28** - -- Added "buildTree" functionality support for Closure and Materialized Path strategies - -Update **2012-02-23** - -- Added a new strategy to support the "Materialized Path" tree model. It works with ODM (MongoDB) and ORM. - -Update **2011-05-07** - -- Tree is now able to act as **closure** tree, this strategy was refactored -and now fully functional. It is much faster for file-folder trees for instance -where you do not care about tree ordering. - -Update **2011-04-11** - -- Made in memory node synchronization, this change does not require clearing the cached nodes after any updates -to nodes, except **recover, verify and removeFromTree** operations. - -Update **2011-02-08** - -- Refactored to support multiple roots -- Changed the repository name, relevant to strategy used -- New [annotations](#annotations) were added - - -Update **2011-02-02** - -- Refactored the Tree to the ability on supporting different tree models -- Changed the repository location in order to support future updates +- **[comfortablynumb](https://github.com/comfortablynumb) Gustavo Falco** for Closure and Materialized Path strategy +- **[everzet](https://github.com/everzet) Kudryashov Konstantin** for TreeLevel implementation +- **[stof](https://github.com/stof) Christophe Coevoet** for getTreeLeafs function **Note:** - After using a NestedTreeRepository functions: **verify, recover, removeFromTree** it is recommended to clear the EntityManager cache because nodes may have changed values in database but not in memory. Flushing dirty nodes can lead to unexpected behaviour. - Closure tree implementation is experimental and not fully functional, so far not documented either -- Public [Tree repository](http://github.com/l3pp4rd/DoctrineExtensions "Tree extension on Github") is available on github -- Last update date: **2012-02-23** - -**Portability:** - -- **Tree** is now available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle) -ported to **Symfony2** by **Christophe Coevoet**, together with all other extensions +- Public [Tree repository](https://github.com/doctrine-extensions/DoctrineExtensions "Tree extension on Github") is available on github This article will cover the basic installation and functionality of **Tree** behavior @@ -73,7 +36,6 @@ Content: - [Including](#including-extension) the extension - Tree [annotations](#annotations) - Entity [example](#entity-mapping) -- [Yaml](#yaml-mapping) mapping example - [Xml](#xml-mapping) mapping example - Basic usage [examples](#basic-examples) - Build [html tree](#html-tree) @@ -86,8 +48,8 @@ Content: ## Setup and autoloading -Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup) -or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example) +Read the [documentation](./annotations.md#em-setup) +or check the [example code](../example) on how to setup and use the extensions in the most optimized way. @@ -95,94 +57,142 @@ on how to setup and use the extensions in the most optimized way. ## Tree Entity example: **Note:** Node interface is not necessary, except in cases where -you need to identify and entity as being a Tree Node. The metadata is loaded only once when the +you need to identify an entity as being a Tree Node. The metadata is loaded only once when the cache is activated -``` php +**Note:** this example is using annotations and attributes for mapping, you should use +one of them, not both. + +```php + * * @ORM\OneToMany(targetEntity="Category", mappedBy="parent") * @ORM\OrderBy({"lft" = "ASC"}) */ + #[ORM\OneToMany(targetEntity: Category::class, mappedBy: 'parent')] + #[ORM\OrderBy(['lft' => 'ASC'])] private $children; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(Category $parent = null) + public function getRoot(): ?self + { + return $this->root; + } + + public function setParent(self $parent = null): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } @@ -191,17 +201,19 @@ class Category -### Tree annotations: +### Tree annotations and attributes: + +These classes can be used either as annotation or as attribute: -- **@Gedmo\Mapping\Annotation\Tree(type="strategy")** this **class annotation** sets the tree strategy by using the **type** parameter. +- **@Gedmo\Mapping\Annotation\Tree(type="strategy")** this **class annotation/attribute** sets the tree strategy by using the **type** parameter. Currently **nested**, **closure** or **materializedPath** strategies are supported. An additional "activateLocking" parameter is available if you use the "Materialized Path" strategy with MongoDB. It's used to activate the locking mechanism (more on that in the corresponding section). - **@Gedmo\Mapping\Annotation\TreeLeft** field is used to store the tree **left** value - **@Gedmo\Mapping\Annotation\TreeRight** field is used to store the tree **right** value - **@Gedmo\Mapping\Annotation\TreeParent** will identify the column as the relation to **parent node** -- **@Gedmo\Mapping\Annotation\TreeLevel** field is used to store the tree **level** -- **@Gedmo\Mapping\Annotation\TreeRoot** field is used to store the tree **root** id value +- **@Gedmo\Mapping\Annotation\TreeLevel(base=0)** field is used to store the tree **level**. The **base** parameter is optional and can be used to set the level of the root nodes to other than 0. +- **@Gedmo\Mapping\Annotation\TreeRoot** field is used to store the tree **root** id value or identify the column as the relation to **root node** - **@Gedmo\Mapping\Annotation\TreePath** (Materialized Path only) field is used to store the **path**. It has an optional parameter "separator" to define the separator used in the path. - **@Gedmo\Mapping\Annotation\TreePathSource** (Materialized Path only) field is used as the source to @@ -210,70 +222,11 @@ optional parameter "separator" to define the separator used in the path. use the locking mechanism with MongoDB. It persists the lock time if a root node is locked (more on that in the corresponding section). - - -## Yaml mapping example - -Yaml mapped Category: **/mapping/yaml/Entity.Category.dcm.yml** - -``` ---- -Entity\Category: - type: entity - repositoryClass: Gedmo\Tree\Entity\Repository\NestedTreeRepository - table: categories - gedmo: - tree: - type: nested - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - lft: - type: integer - gedmo: - - treeLeft - rgt: - type: integer - gedmo: - - treeRight - root: - type: integer - nullable: true - gedmo: - - treeRoot - lvl: - type: integer - gedmo: - - treeLevel - manyToOne: - parent: - targetEntity: Entity\Category - inversedBy: children - joinColumn: - name: parent_id - referencedColumnName: id - onDelete: CASCADE - gedmo: - - treeParent - oneToMany: - children: - targetEntity: Entity\Category - mappedBy: parent - orderBy: - lft: ASC -``` - ## Xml mapping example -``` xml +```xml @@ -295,13 +248,15 @@ Entity\Category: - - - + + + + + @@ -326,7 +281,7 @@ Entity\Category: ### To save some **Categories** and generate tree: -``` php +```php setTitle('Food'); @@ -361,9 +316,9 @@ The result after flush will generate the food tree: ### Using repository functions -``` php +```php getRepository('Entity\Category'); +$repo = $em->getRepository(Category::class); $food = $repo->findOneByTitle('Food'); echo $repo->childCount($food); @@ -383,12 +338,34 @@ $path = $repo->getPath($carrots); 2 => Carrots */ +$stringPath = $repo->getPathAsString([ + 'includeNode' => false, + 'separator' => '/', + 'stringMethod' => 'getTitle', +]); +// $stringPath is 'Food/Vegetables' + // verification and recovery of tree -$repo->verify(); + // can return TRUE if tree is valid, or array of errors found on tree -$repo->recover(); -$em->flush(); // important: flush recovered nodes +$repo->verify(); + // if tree has errors it will try to fix all tree nodes +$repo->recover([ + 'flush' => false, // Do not auto-flush each entity manager after each node is recovered + 'treeRootNode' => $rootNode, // Only recover the $rootNode tree (when you have a forest with multiple root nodes) + 'skipVerify' => false, // Try to verify the tree first and do not attempt recovery if not necessary + 'sortByField' => 'hierarchy', // Reorder sibling nodes by this field during recovery + 'sortDirection' => 'DESC', +]); +$em->flush(); // important: flush recovered nodes, unless you used ['flush' => true] + +// For large trees normal recovery can take a while, use this if speed is a priority. +// No need to flush as it operates outside the entity manager (see phpdoc for side effects) +$repo->recoverFast([ + 'sortByField' => 'hierarchy', // Reorder sibling nodes by this field during recovery + 'sortDirection' => 'DESC', +]); // UNSAFE: be sure to backup before running this method when necessary, if you can use $em->remove($node); // which would cascade to children @@ -406,7 +383,7 @@ $repo->reorder($food, 'title'); ### Inserting node in different positions -``` php +```php setTitle('Food'); @@ -447,9 +424,9 @@ Tree example: Now move **carrots** up by one position -``` php +```php getRepository('Entity\Category'); +$repo = $em->getRepository(Category::class); $carrots = $repo->findOneByTitle('Carrots'); // move it up by one position $repo->moveUp($carrots, 1); @@ -469,9 +446,9 @@ Tree after moving the Carrots up: Moving **carrots** down to the last position -``` php +```php getRepository('Entity\Category'); +$repo = $em->getRepository(Category::class); $carrots = $repo->findOneByTitle('Carrots'); // move it down to the end $repo->moveDown($carrots, true); @@ -489,13 +466,17 @@ Tree after moving the Carrots down as last child: /Fruits ``` -**Note:** the tree repository functions **verify, recover, removeFromTree** +**Note:** the tree repository functions **verify, recover, recoverFast, removeFromTree** will require you to clear the cache of the Entity Manager because left-right values will differ. So after that use **$em->clear();** if you will continue using the nodes after these operations. +In addition, when using **recoverFast** to prioritize speed, you should also keep in mind that it bypasses any locking +scheme and entity event handlers and does not increment the version column. Entities that are already loaded into the +persistence context will NOT be synced with the updated database state. + ### If you need a repository for your TreeNode Entity simply extend it -``` php +```php getRepository('Entity\Category'); +$repo = $em->getRepository(Category::class); $arrayTree = $repo->childrenHierarchy(); ``` @@ -538,26 +521,26 @@ All node children are stored under the **__children** key for each node. To load a tree as a **ul - li** html tree use: -``` php +```php getRepository('Entity\Category'); +$repo = $em->getRepository(Category::class); $htmlTree = $repo->childrenHierarchy( null, /* starting from root nodes */ false, /* true: load all children, false: only direct */ - array( + [ 'decorate' => true, 'representationField' => 'slug', 'html' => true - ) + ] ); ``` ### Customize html tree output -``` php +```php getRepository('Entity\Category'); -$options = array( +$repo = $em->getRepository(Category::class); +$options = [ 'decorate' => true, 'rootOpen' => '
    ', 'rootClose' => '
', @@ -566,7 +549,7 @@ $options = array( 'nodeDecorator' => function($node) { return ''.$node[$field].''; } -); +]; $htmlTree = $repo->childrenHierarchy( null, /* starting from root nodes */ false, /* true: load all children, false: only direct */ @@ -577,51 +560,85 @@ $htmlTree = $repo->childrenHierarchy( ### Generate your own node list -``` php +```php getRepository('Entity\Category'); +$repo = $em->getRepository(Category::class); $query = $entityManager ->createQueryBuilder() ->select('node') - ->from('Entity\Category', 'node') + ->from(Category::class, 'node') ->orderBy('node.root, node.lft', 'ASC') ->where('node.root = 1') ->getQuery() ; -$options = array('decorate' => true); +$options = ['decorate' => true]; $tree = $repo->buildTree($query->getArrayResult(), $options); ``` ### Using routes in decorator, show only selected items, return unlimited levels items as 2 levels -``` php +```php childrenHierarchy(null,false,array('decorate' => true, - 'rootOpen' => function($tree) { - if(count($tree) && ($tree[0]['lvl'] == 0)){ - return '
'; - } - }, - 'rootClose' => function($child) { - if(count($child) && ($child[0]['lvl'] == 0)){ - return '
'; - } - }, - 'childOpen' => '', - 'childClose' => '', - 'nodeDecorator' => function($node) use (&$controller) { - if($node['lvl'] == 1) { - return '

'.$node['title'].'

'; - }elseif($node["isVisibleOnHome"]) { - return '$node['id'])).'">'.$node['title'].' '; - } - } - )); +$tree = $root->childrenHierarchy(null, false, [ + 'decorate' => true, + 'rootOpen' => static function (array $tree): ?string { + if ([] !== $tree && 0 == $tree[0]['lvl']) { + return '
'; + } + + return null; + }, + 'rootClose' => static function (array $child): ?string { + if ([] !== $child && 0 == $child[0]['lvl']) { + return '
'; + } + + return null; + }, + 'childOpen' => '', + 'childClose' => '', + 'nodeDecorator' => static function (array $node) use (&$controller): ?string { + if (1 == $node['lvl']) { + return '

'.$node['title'].'

'; + } + + if ($node["isVisibleOnHome"]) { + return '$node['id']]).'">'.$node['title'].' '; + } + + return null; + } +]); ``` +## Building trees from your entities + +You can use the `childrenHierarchy` method to build an array tree from your result set. +However, sometimes it is more convenient to work with the entities directly. The `TreeObjectHydrator` +lets you build a tree from your entities instead, without triggering any more queries. + +First, you have to register the hydrator in your Doctrine entity manager. + +```php +getConfiguration()->addCustomHydrationMode('tree', 'Gedmo\Tree\Hydrator\ORM\TreeObjectHydrator'); +``` + +The hydrator requires the `HINT_INCLUDE_META_COLUMNS` query hint. Without it the hydrator will not work! +Other than that, the usage is straight-forward. + +```php +getRepository(Category::class); + +$tree = $repo->createQueryBuilder('node')->getQuery() + ->setHint(\Doctrine\ORM\Query::HINT_INCLUDE_META_COLUMNS, true) + ->getResult('tree'); +``` + ## Advanced examples: ### Nesting Translatable and Sluggable extensions @@ -630,7 +647,7 @@ If you want to attach **TranslatableListener** and also add it to EventManager a the **SluggableListener** and **TreeListener**. It is important because slug must be generated first before the creation of it`s translation. -``` php +```php addEventSubscriber($translatableListener); And the Entity should look like: -``` php +```php + * * @ORM\OneToMany(targetEntity="Category", mappedBy="parent") */ + #[ORM\OneToMany(targetEntity: Category::class, mappedBy: 'parent')] private $children; /** + * @var string|null + * * @Gedmo\Translatable * @Gedmo\Slug * @ORM\Column(name="slug", type="string", length=128) */ + #[ORM\Column(name: 'slug', type: Types::STRING, length: 128)] + #[Gedmo\Translatable] + #[Gedmo\Slug] private $slug; - public function getId() + public function getId(): ?int { return $this->id; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(Category $parent) + public function getRoot(): ?self + { + return $this->root; + } + + public function setParent(self $parent = null): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } } ``` -Yaml mapped Category: **/mapping/yaml/Entity.Category.dcm.yml** - -``` ---- -Entity\Category: - type: entity - repositoryClass: Gedmo\Tree\Entity\Repository\NestedTreeRepository - table: categories - gedmo: - tree: - type: nested - id: - id: - type: integer - generator: - strategy: AUTO - fields: - title: - type: string - length: 64 - gedmo: - - translatable - - sluggable - lft: - type: integer - gedmo: - - treeLeft - rgt: - type: integer - gedmo: - - treeRight - lvl: - type: integer - gedmo: - - treeLevel - slug: - type: string - length: 128 - gedmo: - - translatable - - slug - manyToOne: - parent: - targetEntity: Entity\Category - inversedBy: children - joinColumn: - name: parent_id - referencedColumnName: id - onDelete: CASCADE - gedmo: - - treeParent - oneToMany: - children: - targetEntity: Entity\Category - mappedBy: parent -``` - **Note:** If you use dql without object hydration, the nodes will not be translated, because the postLoad event never will be triggered @@ -827,7 +843,7 @@ variations of the field types, including the ORM and ODM for MongoDB ones). ### ORM Entity example (Annotations) -``` php +```php setTitle('Food'); @@ -1112,7 +1071,7 @@ If it is locked, then it throws an exception of type "Gedmo\Exception\TreeLockin it locks the tree and proceeds with the modification. After all the modifications are done, the lock is freed. If, for some reason, the lock couldn't get freed, there's a lock timeout configured with a default time of 3 seconds. -You can change this value using the **lockingTimeout** parameter under the Tree annotation (or equivalent in XML and YML). +You can change this value using the **lockingTimeout** parameter under the Tree attribute (or equivalent in annotation and XML). You must pass a value in seconds to this parameter. @@ -1121,11 +1080,11 @@ You must pass a value in seconds to this parameter. ## Closure Table To be able to use this strategy, you'll need an additional entity which represents the closures. We already provide you an abstract -entity, so you only need to extend it. +entity, so you need to extend from it and add mapping information for ancestor and descendant. ### Closure Entity -``` php +```php id; @@ -1240,8 +1224,8 @@ And that's it! There are repository methods that are available for you in all the strategies: * **getRootNodes** / **getRootNodesQuery** / **getRootNodesQueryBuilder**: Returns an array with the available root nodes. Arguments: - - *sortByField*: An optional field to order the root nodes. Defaults to "null". - - *direction*: In case the first argument is used, you can pass the direction here: "asc" or "desc". Defaults to "asc". + - *sortByField*: array || string - An optional array of fields or field to order the root nodes. Defaults to "null". + - *direction*: array || string - In case the first argument is used, you can pass the direction here: array of values or single value: "asc" or "desc". Defaults to "asc". * **getChildren** / **getChildrenQuery** / **getChildrenQueryBuilder**: Returns an array of children nodes. Arguments: - *node*: If you pass a node, the method will return its children. Defaults to "null" (this means it will return ALL nodes). - *direct*: If you pass true as a value for this argument, you'll get only the direct children of the node @@ -1249,6 +1233,9 @@ There are repository methods that are available for you in all the strategies: - *sortByField*: An optional field to sort the children. Defaults to "null". - *direction*: If you use the "sortByField" argument, this allows you to set the direction: "asc" or "desc". Defaults to "asc". - *includeNode*: Using "true", this argument allows you to include in the result the node you passed as the first argument. Defaults to "false". +* **getPath** / **getPathQuery** / **getPathQueryBuilder** / **getPathAsString**: Return the tree path of Nodes to a given node + (not all available in every strategy). Arguments: + - *includeNode*: Whether to include the given node itself. Defaults to true. * **childrenHierarchy**: This useful method allows you to build an array of nodes representing the hierarchy of a tree. Arguments: - *node*: If you pass a node, the method will return its children. Defaults to "null" (this means it will return ALL nodes). - *direct*: If you pass true as a value for this argument, you'll get only the direct children of the node @@ -1257,7 +1244,7 @@ There are repository methods that are available for you in all the strategies: * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string * rootOpen: string || Closure ('\') - branch start, closure will be given $children as a parameter * rootClose: string ('\') - branch close - * childStart: string || Closure ('\') - start of node, closure will be given $node as a parameter + * childOpen: string || Closure ('\') - start of node, closure will be given $node as a parameter * childClose: string ('\') - close of node * childSort: array || keys allowed: field: field to sort on, dir: direction. 'asc' or 'desc' - *includeNode*: Using "true", this argument allows you to include in the result the node you passed as the first argument. Defaults to "false". diff --git a/doc/upgrading/upgrade-v2.4-to-v3.0.md b/doc/upgrading/upgrade-v2.4-to-v3.0.md new file mode 100644 index 0000000000..5b7db50deb --- /dev/null +++ b/doc/upgrading/upgrade-v2.4-to-v3.0.md @@ -0,0 +1,39 @@ +# Upgrading Doctrine Extensions: from v2.4.x to v3.0 + +Doctrine Extensions v3.0 is primarily focused on upgrading toolsets and dependencies, +to make future work easier and more compatible with modern PHP versions. + +Most users will not need significant development time and effort to upgrade to v3.0. +Look for "_Applies To_" notes for when you may need to take action. + +##### Known Issue: Doctrine MongoDB ODM 2.0 Mapping Drivers + +ODM 2.0 made significant changes to parts of their mappers. The YAML driver was removed completely, and the +[XML driver added schema validation](https://github.com/doctrine-extensions/DoctrineExtensions/issues/2055) that does +not allow mixing of native ODM and Extensions elements. + +**YAML and XML mapping users may not be able to use Doctrine Extensions 3.0**, which does not attempt to resolve +these issues at the time. If you use Annotations or PHP mapping drivers, you should be unaffected. + +See [Issue #2055](https://github.com/doctrine-extensions/DoctrineExtensions/issues/2055) on GitHub for more information. +Please leave a message if this affects your project. + +## PHP 7.2 Required + +_Applies To: Everyone_ + +PHP 7.1 is no longer maintained as of December 2019. + +## MongoDB + +_Applies To: Projects using DoctrineExtension with MongoDB_ + +- Requires the `ext-mongodb` PHP extension. Usage of `ext-mongo` is deprecated and will be removed in the next major version. +- Minimum Doctrine MongoDB ODM requirement of 2.0 + +## SoftDeleteable + +_Applies To: Projects with a custom `SoftDeleteableAdapter` implementation_ + +The [`SoftDeleteableAdapter`](/src/SoftDeleteable/Mapping/Event/SoftDeleteableAdapter.php) interface has a new method +for generating the DateTime value. diff --git a/doc/uploadable.md b/doc/uploadable.md index c924a3106f..f52e853404 100644 --- a/doc/uploadable.md +++ b/doc/uploadable.md @@ -1,7 +1,7 @@ -# Uploadable behavior extension for Doctrine 2 +# Uploadable behavior extension for Doctrine **Uploadable** behavior provides the tools to manage the persistence of files with -Doctrine 2, including automatic handling of moving, renaming and removal of files and other features. +Doctrine, including automatic handling of moving, renaming and removal of files and other features. Features: @@ -15,7 +15,6 @@ Content: - [Including](#including-extension) the extension - Entity [example](#entity-mapping) -- [Yaml](#yaml-mapping) mapping example - [Xml](#xml-mapping) mapping example - Usage [examples](#usage) - [Using](#additional-usages) the extension to handle not only uploaded files @@ -25,8 +24,8 @@ Content: ## Setup and autoloading -Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup) -or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example) +Read the [documentation](./annotations.md#em-setup) +or check the [example code](../example) on how to setup and use the extensions in most optimized way. @@ -34,8 +33,11 @@ on how to setup and use the extensions in most optimized way. ## Uploadable Entity example: -### Uploadable annotations: -1. **@Gedmo\Mapping\Annotation\Uploadable** this class annotation tells if a class is Uploadable. Available configuration options: +### Uploadable annotations and attributes: + +These classes can be used either as annotation or as attribute: + +1. **@Gedmo\Mapping\Annotation\Uploadable** this class annotation/attribute tells if a class is Uploadable. Available configuration options: * **allowOverwrite** - If this option is true, it will overwrite a file if it already exists. If you set "false", an exception will be thrown. Default: false * **appendNumber** - If this option is true and "allowOverwrite" is false, in the case that the file already exists, @@ -77,33 +79,38 @@ on how to setup and use the extensions in most optimized way. occurs. If you want to use a custom mime type guesser, see [this](#custom-mime-type-guessers). * **disallowedTypes**: Similar to the option **allowedTypes**, but with this one you configure a "black list" of mime types. If the mime type of the file is on this list, n exception of type "UploadableInvalidMimeTypeException" will be thrown. If you - set this option, you can't set the **allowedTypes** option described next. By default, no validation of mime type + set this option, you can't set the **allowedTypes** option described above. By default, no validation of mime type occurs. If you want to use a custom mime type guesser, see [this](#custom-mime-type-guessers). -2. **@Gedmo\Mapping\Annotation\UploadableFilePath**: This annotation is used to set which field will receive the path - to the file. The field MUST be of type "string". Either this one or UploadableFileName annotation is REQUIRED to be set. -3. **@Gedmo\Mapping\Annotation\UploadableFileName**: This annotation is used to set which field will receive the name - of the file. The field MUST be of type "string". Either this one or UploadableFilePath annotation is REQUIRED to be set. -4. **@Gedmo\Mapping\Annotation\UploadableFileMimeType**: This is an optional annotation used to set which field will +2. **@Gedmo\Mapping\Annotation\UploadableFilePath**: This annotation/attribute is used to set which field will receive the path + to the file. The field MUST be of type "string". Either this one or UploadableFileName annotation/attribute is REQUIRED to be set. +3. **@Gedmo\Mapping\Annotation\UploadableFileName**: This annotation/attribute is used to set which field will receive the name + of the file. The field MUST be of type "string". Either this one or UploadableFilePath annotation/attribute is REQUIRED to be set. +4. **@Gedmo\Mapping\Annotation\UploadableFileMimeType**: This is an optional annotation/attribute used to set which field will receive the mime type of the file as its value. This field MUST be of type "string". -5. **@Gedmo\Mapping\Annotation\UploadableFileSize**: This is an optional annotation used to set which field will +5. **@Gedmo\Mapping\Annotation\UploadableFileSize**: This is an optional annotation/attribute used to set which field will receive the size in bytes of the file as its value. This field MUST be of type "decimal". +**Note:** the examples shown here are using annotations and attributes for mapping, you should use +one of them, not both. + ### Notes about setting the path where the files will be moved: You have three choices to configure the path. You can set a default path on the listener, which will be used on every entity which doesn't have a path or pathMethod defined: -``` php +```php $listener->setDefaultPath('/my/path'); ``` You can use the Uploadable "path" option to set the path: -``` php +```php /** * @ORM\Entity * @Gedmo\Uploadable(path="/my/path") */ +#[ORM\Entity] +#[Gedmo\Uploadable(path: '/my/path')] class File { //... @@ -112,11 +119,13 @@ class File Or you can use the Uploadable "pathMethod" option to set the name of the method which will return the path: -``` php +```php /** * @ORM\Entity * @Gedmo\Uploadable(pathMethod="getPath") */ +#[ORM\Entity] +#[Gedmo\Uploadable(pathMethod: 'getPath')] class File { public function getPath() @@ -136,10 +145,12 @@ cache is activated ### Minimum configuration needed: -``` php +```php - -## Yaml mapping example: - -Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml** - -``` ---- -Entity\File: - type: entity - table: files - gedmo: - uploadable: - allowOverwrite: true - appendNumber: true - path: '/my/path' - pathMethod: getPath - callback: callbackMethod - filenameGenerator: SHA1 - id: - id: - type: integer - generator: - strategy: AUTO - fields: - path: - type: string - gedmo: - - uploadableFilePath - name: - type: string - gedmo: - - uploadableFileName - mimeType: - type: string - gedmo: - - uploadableFileMimeType - size: - type: decimal - gedmo: - - uploadableFileSize -``` - ## Xml mapping example -``` xml +```xml path; } // This returns the filename @@ -407,7 +399,7 @@ class CustomFileInfo implements FileInfoInterface Or you could simply extend the FileInfoArray class and do the following: -``` php +```php use Gedmo\Uploadable\FileInfo\FileInfoArray; class CustomFileInfo extends FileInfoArray @@ -441,7 +433,7 @@ class CustomFileInfo extends FileInfoArray And that's it. Then, instead of getting the file info from the $_FILES array, you would do: -``` php +```php // We set the default path in the listener again $listener->setDefaultPath('/my/path'); @@ -461,7 +453,7 @@ If you want to use your own mime type guesser, you need to implement the interfa which has only one method: "guess($filePath)". Then, you can set the mime type guesser used on the listener in the following way: -``` php +```php $listener->setMimeTypeGuesser(new MyCustomMimeTypeGuesser()); ``` diff --git a/doc/utils/actor-provider.md b/doc/utils/actor-provider.md new file mode 100644 index 0000000000..22ee883b8f --- /dev/null +++ b/doc/utils/actor-provider.md @@ -0,0 +1,56 @@ +# Actor Provider + +The Doctrine Extensions package includes support for an "actor provider" for extensions which use a user value, such as +the blameable or loggable extensions. + +## Index + +- [Getting Started](#getting-started) +- [Benefits of Actor Providers](#benefits-of-actor-providers) + +## Getting Started + +Out of the box, the library does not provide an implementation for the `Gedmo\Tool\ActorProviderInterface`, so you will +need to create a class in your application. Below is an example of an actor provider using Symfony's Security components: + +```php +namespace App\Utils; + +use Gedmo\Tool\ActorProviderInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; + +final class SymfonyActorProvider implements ActorProviderInterface +{ + private TokenStorageInterface $tokenStorage; + + public function __construct(TokenStorageInterface $tokenStorage) + { + $this->tokenStorage = $tokenStorage; + } + + /** + * @return object|string|null + */ + public function getActor() + { + $token = $this->tokenStorage->getToken(); + + return $token ? $token->getUser() : null; + } +} +``` + +Once you've created your actor provider, you can inject it into the listeners for supported extensions by calling +the `setActorProvider` method. + +```php +/** Gedmo\Blameable\BlameableListener $listener */ +$listener->setActorProvider($provider); +``` + +## Benefits of Actor Providers + +Unlike the previously existing APIs for the extensions which support user references, actor providers allow lazily +resolving the user value when it is needed instead of eagerly fetching it when the listener is created. Actor providers +would also integrate nicely with long-running processes such as FrankenPHP where the provider can be reset between +requests. diff --git a/doc/utils/ip-address-provider.md b/doc/utils/ip-address-provider.md new file mode 100644 index 0000000000..6ceed0137b --- /dev/null +++ b/doc/utils/ip-address-provider.md @@ -0,0 +1,45 @@ +# IP Address Provider + +The Doctrine Extensions package includes support for an "IP address provider" for extensions which use an IP address value, such as +the IP traceable extension. + +## Index + +- [Getting Started](#getting-started) + +## Getting Started + +Out of the box, the library does not provide an implementation for the `Gedmo\Tool\IpAddressProviderInterface`, so you will +need to create a class in your application. Below is an example of an IP address provider using Symfony's HttpFoundation component: + +```php +namespace App\Utils; + +use Gedmo\Tool\IpAddressProviderInterface; +use Symfony\Component\HttpFoundation\RequestStack; + +final class RequestIpAddressProvider implements IpAddressProviderInterface +{ + private RequestStack $requestStack; + + public function __construct(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + } + + public function getAddress(): ?string + { + $request = $this->requestStack->getMainRequest(); + + return $request ? $request->getClientIp() : null; + } +} +``` + +Once you've created your IP address provider, you can inject it into the listeners for supported extensions by calling +the `setIpAddressProvider` method. + +```php +/** @var Gedmo\IpTraceable\IpTraceableListener $listener */ +$listener->setIpAddressProvider($provider); +``` diff --git a/doc/zendframework2.md b/doc/zendframework2.md deleted file mode 100644 index 4eec20edda..0000000000 --- a/doc/zendframework2.md +++ /dev/null @@ -1,97 +0,0 @@ -## Using Gedmo Doctrine Extensions in Zend Framework 2 - -Assuming you are familiar with [DoctrineModule](https://github.com/doctrine/DoctrineModule) (if not, you should definitely start there!), integrating Doctrine Extensions with Zend Framework 2 application is super-easy. - -### Composer - -Add DoctrineModule, DoctrineORMModule and DoctrineExtensions to composer.json file: - -```json -{ - "require": { - "php": ">=5.3.3", - "zendframework/zendframework": "2.1.*", - "doctrine/doctrine-module": "0.*", - "doctrine/doctrine-orm-module": "0.*", - "gedmo/doctrine-extensions": "2.3.*", - } -} -``` - -Then run `composer.phar update`. - -### Configuration - -Once libraries are installed, you can tell Doctrine which behaviors you want to use, by declaring appropriate subscribers in Event Manager settings. Together with [entity mapping options](https://github.com/doctrine/DoctrineORMModule#entities-settings), your module configuration file should look like following: - -```php -return array( - 'doctrine' => array( - 'eventmanager' => array( - 'orm_default' => array( - 'subscribers' => array( - - // pick any listeners you need - 'Gedmo\Tree\TreeListener', - 'Gedmo\Timestampable\TimestampableListener', - 'Gedmo\Sluggable\SluggableListener', - 'Gedmo\Loggable\LoggableListener', - 'Gedmo\Sortable\SortableListener' - ), - ), - ), - 'driver' => array( - 'my_driver' => array( - 'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver', - 'cache' => 'array', - 'paths' => array(__DIR__ . '/../src/MyModule/Entity') - ), - 'orm_default' => array( - 'drivers' => array( - 'MyModule\Entity' => 'my_driver' - ), - ), - ), - ), -); -``` - -That's it! From now on you can use Gedmo annotations, just as it is described in [documentation](https://github.com/mtymek/DoctrineExtensions/blob/master/doc/annotations.md). - -#### Note: You may need to provide additional settings for some of the available listeners. - -For instance, `Translatable` requires additional metadata driver in order to manage translation tables: - -```php -return array( - 'doctrine' => array( - 'eventmanager' => array( - 'orm_default' => array( - 'subscribers' => array( - 'Gedmo\Translatable\TranslatableListener', - ), - ), - ), - 'driver' => array( - 'my_driver' => array( - 'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver', - 'cache' => 'array', - 'paths' => array(__DIR__ . '/../src/MyModule/Entity') - ), - 'translatable_metadata_driver' => array( - 'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver', - 'cache' => 'array', - 'paths' => array( - 'vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity', - ), - ), - 'orm_default' => array( - 'drivers' => array( - 'MyModule\Entity' => 'my_driver', - 'Gedmo\Translatable\Entity' => 'translatable_metadata_driver', - ), - ), - ), - ), -); -``` diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd new file mode 100644 index 0000000000..3f38d9744e --- /dev/null +++ b/doctrine-mapping.xsd @@ -0,0 +1,5 @@ + + + + + diff --git a/example/app/Command/PrintCategoryTranslationTreeCommand.php b/example/app/Command/PrintCategoryTranslationTreeCommand.php new file mode 100644 index 0000000000..43d1180fb9 --- /dev/null +++ b/example/app/Command/PrintCategoryTranslationTreeCommand.php @@ -0,0 +1,106 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Command; + +use App\Entity\Category; +use App\Entity\CategoryTranslation; +use App\Entity\Repository\CategoryRepository; +use Doctrine\ORM\Query; +use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper; +use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; +use Gedmo\Translatable\TranslatableListener; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +final class PrintCategoryTranslationTreeCommand extends Command +{ + protected static $defaultName = 'app:print-category-translation-tree'; + protected static $defaultDescription = 'Seeds an example category tree with translations and prints the tree.'; + + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var EntityManagerHelper $helper */ + $helper = $this->getHelper('em'); + + $em = $helper->getEntityManager(); + + /** @var CategoryRepository $repository */ + $repository = $em->getRepository(Category::class); + + /** @var Category|null $food */ + $food = $repository->findOneByTitle('Food'); + + // If we don't have our examples in the database already, seed them + if (null === $food) { + $food = new Category(); + $food->setTitle('Food'); + $food->addTranslation(new CategoryTranslation('lt', 'title', 'Maistas')); + + $fruits = new Category(); + $fruits->setParent($food); + $fruits->setTitle('Fruits'); + $fruits->addTranslation(new CategoryTranslation('lt', 'title', 'Vaisiai')); + + $apple = new Category(); + $apple->setParent($fruits); + $apple->setTitle('Apple'); + $apple->addTranslation(new CategoryTranslation('lt', 'title', 'Obuolys')); + + $milk = new Category(); + $milk->setParent($food); + $milk->setTitle('Milk'); + $milk->addTranslation(new CategoryTranslation('lt', 'title', 'Pienas')); + + $em->persist($food); + $em->persist($milk); + $em->persist($fruits); + $em->persist($apple); + $em->flush(); + } + + // Create a query to fetch the tree nodes + $query = $em->createQueryBuilder() + ->select('node') + ->from(Category::class, 'node') + ->orderBy('node.root') + ->addOrderBy('node.lft') + ->getQuery() + ; + + // Set the hint to translate nodes + $query->setHint( + Query::HINT_CUSTOM_OUTPUT_WALKER, + TranslationWalker::class + ); + + $treeDecorationOptions = [ + 'decorate' => true, + 'rootOpen' => '', + 'rootClose' => '', + 'childOpen' => '', + 'childClose' => '', + 'nodeDecorator' => static fn ($node): string => str_repeat('-', $node['level']).$node['title'].PHP_EOL, + ]; + + // Build the tree in English + $output->writeln('English:'); + $output->writeln($repository->buildTree($query->getArrayResult(), $treeDecorationOptions)); + + // Change the locale and build the tree in Lithuanian + $query->setHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'lt'); + $output->writeln('Lithuanian:'); + $output->writeln($repository->buildTree($query->getArrayResult(), $treeDecorationOptions)); + + return 0; + } +} diff --git a/example/app/Entity/Category.php b/example/app/Entity/Category.php index c8e2df15a6..bd51a48b90 100644 --- a/example/app/Entity/Category.php +++ b/example/app/Entity/Category.php @@ -1,16 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Entity; + +use App\Entity\Repository\CategoryRepository; use Doctrine\Common\Collections\ArrayCollection; -use Gedmo\Mapping\Annotation as Gedmo; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @Gedmo\Tree(type="nested") + * * @ORM\Table(name="ext_categories") - * @ORM\Entity(repositoryClass="Entity\Repository\CategoryRepository") - * @Gedmo\TranslationEntity(class="Entity\CategoryTranslation") + * @ORM\Entity(repositoryClass="App\Entity\Repository\CategoryRepository") + * + * @Gedmo\TranslationEntity(class="App\Entity\CategoryTranslation") */ +#[Gedmo\Tree(type: 'nested')] +#[ORM\Table(name: 'ext_categories')] +#[ORM\Entity(repositoryClass: CategoryRepository::class)] +#[Gedmo\TranslationEntity(class: CategoryTranslation::class)] class Category { /** @@ -18,94 +36,143 @@ class Category * @ORM\Id * @ORM\GeneratedValue */ + #[ORM\Column(type: Types::INTEGER)] + #[ORM\Id] + #[ORM\GeneratedValue] private $id; /** + * @var string|null + * * @Gedmo\Translatable + * * @ORM\Column(length=64) */ + #[Gedmo\Translatable] + #[ORM\Column(length: 64)] private $title; /** + * @var string|null + * * @Gedmo\Translatable + * * @ORM\Column(type="text", nullable=true) */ + #[Gedmo\Translatable] + #[ORM\Column(type: Types::TEXT, nullable: true)] private $description; /** + * @var string|null + * * @Gedmo\Translatable * @Gedmo\Slug(fields={"created", "title"}) + * * @ORM\Column(length=64, unique=true) */ + #[Gedmo\Translatable] + #[Gedmo\Slug(fields: ['created', 'title'])] + #[ORM\Column(length: 64, unique: true)] private $slug; /** * @Gedmo\TreeLeft + * * @ORM\Column(type="integer") */ + #[Gedmo\TreeLeft] + #[ORM\Column(type: Types::INTEGER)] private $lft; /** * @Gedmo\TreeRight + * * @ORM\Column(type="integer") */ + #[Gedmo\TreeRight] + #[ORM\Column(type: Types::INTEGER)] private $rgt; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="Category", inversedBy="children") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") */ + #[Gedmo\TreeParent] + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] private $parent; /** * @Gedmo\TreeRoot + * * @ORM\Column(type="integer", nullable=true) */ + #[Gedmo\TreeRoot] + #[ORM\Column(type: Types::INTEGER, nullable: true)] private $root; /** * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer") */ + #[Gedmo\TreeLevel] + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] private $level; /** * @ORM\OneToMany(targetEntity="Category", mappedBy="parent") */ - private $children; + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; /** * @Gedmo\Timestampable(on="create") + * * @ORM\Column(type="datetime") */ + #[Gedmo\Timestampable(on: 'create')] + #[ORM\Column(type: Types::DATETIME_MUTABLE)] private $created; /** * @Gedmo\Timestampable(on="update") + * * @ORM\Column(type="datetime") */ + #[Gedmo\Timestampable(on: 'update')] + #[ORM\Column(type: Types::DATETIME_MUTABLE)] private $updated; /** * @Gedmo\Blameable(on="create") + * * @ORM\Column(type="string") */ + #[Gedmo\Blameable(on: 'create')] + #[ORM\Column(type: Types::STRING)] private $createdBy; /** * @Gedmo\Blameable(on="update") + * * @ORM\Column(type="string") */ + #[Gedmo\Blameable(on: 'update')] + #[ORM\Column(type: Types::STRING)] private $updatedBy; /** * @ORM\OneToMany( - * targetEntity="CategoryTranslation", - * mappedBy="object", - * cascade={"persist", "remove"} + * targetEntity="CategoryTranslation", + * mappedBy="object", + * cascade={"persist", "remove"} * ) */ + #[ORM\OneToMany(targetEntity: CategoryTranslation::class, mappedBy: 'object', cascade: ['persist', 'remove'])] private $translations; public function __construct() @@ -114,6 +181,11 @@ public function __construct() $this->translations = new ArrayCollection(); } + public function __toString() + { + return $this->getTitle(); + } + public function getTranslations() { return $this->translations; @@ -211,9 +283,4 @@ public function getUpdatedBy() { return $this->updatedBy; } - - public function __toString() - { - return $this->getTitle(); - } } diff --git a/example/app/Entity/CategoryTranslation.php b/example/app/Entity/CategoryTranslation.php index 27182e2d91..2f28b50045 100644 --- a/example/app/Entity/CategoryTranslation.php +++ b/example/app/Entity/CategoryTranslation.php @@ -1,6 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation; @@ -13,10 +22,21 @@ * })} * ) */ +#[ORM\Entity] +#[ORM\Table(name: 'category_translations')] +#[ORM\UniqueConstraint(name: 'lookup_unique_idx', columns: ['locale', 'object_id', 'field'])] class CategoryTranslation extends AbstractPersonalTranslation { /** - * Convinient constructor + * @ORM\ManyToOne(targetEntity="Category", inversedBy="translations") + * @ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE") + */ + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'translations')] + #[ORM\JoinColumn(name: 'object_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + protected $object; + + /** + * Convenient constructor * * @param string $locale * @param string $field @@ -28,10 +48,4 @@ public function __construct($locale, $field, $value) $this->setField($field); $this->setContent($value); } - - /** - * @ORM\ManyToOne(targetEntity="Category", inversedBy="translations") - * @ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE") - */ - protected $object; } diff --git a/example/app/Entity/Repository/CategoryRepository.php b/example/app/Entity/Repository/CategoryRepository.php index 4a143c8119..543ea30ee5 100644 --- a/example/app/Entity/Repository/CategoryRepository.php +++ b/example/app/Entity/Repository/CategoryRepository.php @@ -1,9 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Entity\Repository; + +use App\Entity\Category; use Gedmo\Tree\Entity\Repository\NestedTreeRepository; -class CategoryRepository extends NestedTreeRepository +/** + * @template-extends NestedTreeRepository + */ +final class CategoryRepository extends NestedTreeRepository { } diff --git a/example/bin/console b/example/bin/console index 324f1632f2..6073cf9307 100755 --- a/example/bin/console +++ b/example/bin/console @@ -1,6 +1,7 @@ #!/usr/bin/env php run(); - diff --git a/example/bin/console.php b/example/bin/console.php index 9f9ca17191..824d8c07d6 100644 --- a/example/bin/console.php +++ b/example/bin/console.php @@ -1,38 +1,36 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use App\Command\PrintCategoryTranslationTreeCommand; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Tools\Console\ConsoleRunner; +use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider; +use Gedmo\DoctrineExtensions; +use Symfony\Component\Console\Application; + +/** @var EntityManager $em */ $em = include __DIR__.'/../em.php'; -$cli = new Symfony\Component\Console\Application('My CLI interface', '1.0.0'); +$entityManagerProvider = new SingleManagerProvider($em); + +$cli = new Application('Doctrine Extensions Example Application', DoctrineExtensions::VERSION); $cli->setCatchExceptions(true); -// commands -$cli->addCommands(array( - // DBAL Commands - new Doctrine\DBAL\Tools\Console\Command\RunSqlCommand(), - new Doctrine\DBAL\Tools\Console\Command\ImportCommand(), - - // ORM Commands - new Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand(), - new Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand(), - new Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand(), - new Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand(), - new Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand(), - new Doctrine\ORM\Tools\Console\Command\SchemaTool\DropCommand(), - new Doctrine\ORM\Tools\Console\Command\EnsureProductionSettingsCommand(), - new Doctrine\ORM\Tools\Console\Command\ConvertDoctrine1SchemaCommand(), - new Doctrine\ORM\Tools\Console\Command\GenerateRepositoriesCommand(), - new Doctrine\ORM\Tools\Console\Command\GenerateEntitiesCommand(), - new Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand(), - new Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand(), - new Doctrine\ORM\Tools\Console\Command\RunDqlCommand(), - new Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand(), -)); -// helpers -$helpers = array( - 'db' => new Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()), - 'em' => new Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em), -); -foreach ($helpers as $name => $helper) { - $cli->getHelperSet()->set($helper, $name); -} +$cli->setHelperSet(ConsoleRunner::createHelperSet($em)); + +// Use the ORM's console runner to register the default commands available from the DBAL and ORM for the environment +ConsoleRunner::addCommands($cli, $entityManagerProvider); + +// Register our example app commands +$cli->addCommands([ + new PrintCategoryTranslationTreeCommand(), +]); return $cli; diff --git a/example/em.php b/example/em.php index dd8831fd2a..8f9ea8d484 100644 --- a/example/em.php +++ b/example/em.php @@ -1,122 +1,176 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ -// connection args, modify at will -$connection = array( + +use Composer\Autoload\ClassLoader; +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\PsrCachedReader; +use Doctrine\Common\EventManager; +use Doctrine\DBAL\DriverManager; +use Doctrine\ORM\Configuration; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\Blameable\BlameableListener; +use Gedmo\DoctrineExtensions; +use Gedmo\Mapping\Driver\AttributeReader; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Timestampable\TimestampableListener; +use Gedmo\Translatable\TranslatableListener; +use Gedmo\Tree\TreeListener; +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +// this entity manager configuration works with the Doctrine DBAL and ORM. +// Regarding the AnnotationDriver setup, it most probably will be changed into +// XML because the annotation driver fails to read other classes in same namespace. + +// Database connection configuration, modify at will +// Below is an example MySQL configuration +$connection = [ 'host' => '127.0.0.1', 'port' => 3306, 'user' => 'root', 'password' => null, - 'dbname' => 'test', + 'dbname' => 'doctrine_extensions_example', 'driver' => 'pdo_mysql', -); + 'charset' => 'utf8mb4', +]; + if (!file_exists(__DIR__.'/../vendor/autoload.php')) { - die('cannot find vendors, read README.md how to use composer'); + echo 'Composer has not been properly set up, please read the README.md file for setup instructions.'; + + exit(1); } -// First of all autoloading of vendors + +/** @var ClassLoader $loader */ $loader = require __DIR__.'/../vendor/autoload.php'; -// gedmo extensions -$loader->add('Gedmo', __DIR__.'/../lib'); +// Register the example app with the autoloader +$loader->addPsr4('App\\', __DIR__.'/app'); -// autoloader for Entity namespace -$loader->add('Entity', __DIR__.'/app'); +// Define our global cache backend for the application. +// For larger applications, you may use multiple cache pools to store cacheable data in different locations. +$cache = new ArrayAdapter(); -// ensure standard doctrine annotations are registered -Doctrine\Common\Annotations\AnnotationRegistry::registerFile( - __DIR__.'/../vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php' -); +$annotationReader = null; +$extensionReader = null; -// Second configure ORM -// globally used cache driver, in production use APC or memcached -$cache = new Doctrine\Common\Cache\ArrayCache(); -// standard annotation reader -$annotationReader = new Doctrine\Common\Annotations\AnnotationReader(); -$cachedAnnotationReader = new Doctrine\Common\Annotations\CachedReader( - $annotationReader, // use reader - $cache // and a cache driver -); -// create a driver chain for metadata reading -$driverChain = new Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain(); -// load superclass metadata mapping only, into driver chain -// also registers Gedmo annotations.NOTE: you can personalize it -Gedmo\DoctrineExtensions::registerAbstractMappingIntoDriverChainORM( - $driverChain, // our metadata driver chain, to hook into - $cachedAnnotationReader // our cached annotation reader -); +// For PHP 8, we will provide the extensions an attribute reader, while PHP 7 will require the annotation reader +// (which will only be created when `doctrine/annotations` is installed) +if (PHP_VERSION_ID >= 80000) { + $extensionReader = new AttributeReader(); +} + +// Build the annotation reader for the application when the `doctrine/annotations` package is installed. +// By default, we will use a decorated reader supporting a backend cache. +if (class_exists(AnnotationReader::class)) { + $extensionReader = $annotationReader = new PsrCachedReader( + new AnnotationReader(), + $cache + ); +} + +// Create the mapping driver chain that will be used to read metadata from our various sources. +$mappingDriver = new MappingDriverChain(); -// now we want to register our application entities, -// for that we need another metadata driver used for Entity namespace -$annotationDriver = new Doctrine\ORM\Mapping\Driver\AnnotationDriver( - $cachedAnnotationReader, // our cached annotation reader - array(__DIR__.'/app/Entity') // paths to look in +// Load the superclass metadata mapping for the extensions into the driver chain. +// Internally, this will also register the Doctrine Extensions annotations. +DoctrineExtensions::registerAbstractMappingIntoDriverChainORM( + $mappingDriver, + $annotationReader ); -// NOTE: driver for application Entity can be different, Yaml, Xml or whatever -// register annotation driver for our application Entity fully qualified namespace -$driverChain->addDriver($annotationDriver, 'Entity'); -// general ORM configuration -$config = new Doctrine\ORM\Configuration(); -$config->setProxyDir(sys_get_temp_dir()); -$config->setProxyNamespace('Proxy'); -$config->setAutoGenerateProxyClasses(false); // this can be based on production config. -// register metadata driver -$config->setMetadataDriverImpl($driverChain); -// use our allready initialized cache driver -$config->setMetadataCacheImpl($cache); -$config->setQueryCacheImpl($cache); - -// Third, create event manager and hook prefered extension listeners -$evm = new Doctrine\Common\EventManager(); -// gedmo extension listeners - -// sluggable -$sluggableListener = new Gedmo\Sluggable\SluggableListener(); -// you should set the used annotation reader to listener, to avoid creating new one for mapping drivers -$sluggableListener->setAnnotationReader($cachedAnnotationReader); -$evm->addEventSubscriber($sluggableListener); - -// tree -$treeListener = new Gedmo\Tree\TreeListener(); -$treeListener->setAnnotationReader($cachedAnnotationReader); -$evm->addEventSubscriber($treeListener); - -// loggable, not used in example -//$loggableListener = new Gedmo\Loggable\LoggableListener; -//$loggableListener->setAnnotationReader($cachedAnnotationReader); -//$loggableListener->setUsername('admin'); -//$evm->addEventSubscriber($loggableListener); - -// timestampable -$timestampableListener = new Gedmo\Timestampable\TimestampableListener(); -$timestampableListener->setAnnotationReader($cachedAnnotationReader); -$evm->addEventSubscriber($timestampableListener); - -// blameable - -$blameableListener = new \Gedmo\Blameable\BlameableListener(); -$blameableListener->setAnnotationReader($cachedAnnotationReader); +// Register the application entities to our driver chain. +// Our application uses Annotations or Attributes for mapping, but you can also use XML. +if (PHP_VERSION_ID >= 80000) { + $mappingDriver->addDriver( + new AttributeDriver( + [__DIR__.'/app/Entity'] + ), + 'App\Entity' + ); +} else { + $mappingDriver->addDriver( + new AnnotationDriver( + $annotationReader, + [__DIR__.'/app/Entity'] + ), + 'App\Entity' + ); +} + +// Next, we will create the event manager and register the listeners for the extensions we will be using. +$eventManager = new EventManager(); + +// Sluggable extension +$sluggableListener = new SluggableListener(); +$sluggableListener->setAnnotationReader($extensionReader); +$sluggableListener->setCacheItemPool($cache); +$eventManager->addEventSubscriber($sluggableListener); + +// Tree extension +$treeListener = new TreeListener(); +$treeListener->setAnnotationReader($extensionReader); +$treeListener->setCacheItemPool($cache); +$eventManager->addEventSubscriber($treeListener); + +// Loggable extension, not used in example +// $loggableListener = new Gedmo\Loggable\LoggableListener; +// $loggableListener->setAnnotationReader($extensionReader); +// $loggableListener->setCacheItemPool($cache); +// $loggableListener->setUsername('admin'); +// $eventManager->addEventSubscriber($loggableListener); + +// Timestampable extension +$timestampableListener = new TimestampableListener(); +$timestampableListener->setAnnotationReader($extensionReader); +$timestampableListener->setCacheItemPool($cache); +$eventManager->addEventSubscriber($timestampableListener); + +// Blameable extension +$blameableListener = new BlameableListener(); +$blameableListener->setAnnotationReader($extensionReader); +$blameableListener->setCacheItemPool($cache); $blameableListener->setUserValue('MyUsername'); // determine from your environment -$evm->addEventSubscriber($blameableListener); +$eventManager->addEventSubscriber($blameableListener); -// translatable -$translatableListener = new Gedmo\Translatable\TranslatableListener(); -// current translation locale should be set from session or hook later into the listener -// most important, before entity manager is flushed +// Translatable +$translatableListener = new TranslatableListener(); + +// The current translation locale should be set from session or some other request data, +// but most importantly, it must be set before the entity manager is flushed. $translatableListener->setTranslatableLocale('en'); $translatableListener->setDefaultLocale('en'); -$translatableListener->setAnnotationReader($cachedAnnotationReader); -$evm->addEventSubscriber($translatableListener); - -// sortable, not used in example -//$sortableListener = new Gedmo\Sortable\SortableListener; -//$sortableListener->setAnnotationReader($cachedAnnotationReader); -//$evm->addEventSubscriber($sortableListener); - -// mysql set names UTF-8 if required -$evm->addEventSubscriber(new Doctrine\DBAL\Event\Listeners\MysqlSessionInit()); -// Finally, create entity manager -return Doctrine\ORM\EntityManager::create($connection, $config, $evm); +$translatableListener->setAnnotationReader($extensionReader); +$translatableListener->setCacheItemPool($cache); +$eventManager->addEventSubscriber($translatableListener); + +// Sortable extension, not used in example +// $sortableListener = new Gedmo\Sortable\SortableListener; +// $sortableListener->setAnnotationReader($extensionReader); +// $sortableListener->setCacheItemPool($cache); +// $eventManager->addEventSubscriber($sortableListener); + +// Now we will build our ORM configuration. +$config = new Configuration(); +$config->setProxyDir(sys_get_temp_dir()); +$config->setProxyNamespace('Proxy'); +$config->setAutoGenerateProxyClasses(false); +$config->setMetadataDriverImpl($mappingDriver); +$config->setMetadataCache($cache); +$config->setQueryCache($cache); +$config->setResultCache($cache); + +$connection = DriverManager::getConnection($connection, $config); + +// Finally, we create and return the entity manager + +return new EntityManager($connection, $config, $eventManager); diff --git a/example/run.php b/example/run.php deleted file mode 100644 index c6619dc5f8..0000000000 --- a/example/run.php +++ /dev/null @@ -1,80 +0,0 @@ -getRepository('Entity\Category'); -$food = $repository->findOneByTitle('Food'); -if (!$food) { - // lets create some categories - $food = new Entity\Category(); - $food->setTitle('Food'); - $food->addTranslation(new Entity\CategoryTranslation('lt', 'title', 'Maistas')); - - $fruits = new Entity\Category(); - $fruits->setParent($food); - $fruits->setTitle('Fruits'); - $fruits->addTranslation(new Entity\CategoryTranslation('lt', 'title', 'Vaisiai')); - - $apple = new Entity\Category(); - $apple->setParent($fruits); - $apple->setTitle('Apple'); - $apple->addTranslation(new Entity\CategoryTranslation('lt', 'title', 'Obuolys')); - - $milk = new Entity\Category(); - $milk->setParent($food); - $milk->setTitle('Milk'); - $milk->addTranslation(new Entity\CategoryTranslation('lt', 'title', 'Pienas')); - - $em->persist($food); - $em->persist($milk); - $em->persist($fruits); - $em->persist($apple); - $em->flush(); -} - -// create query to fetch tree nodes -$query = $em - ->createQueryBuilder() - ->select('node') - ->from('Entity\Category', 'node') - ->orderBy('node.root, node.lft', 'ASC') - ->getQuery() -; -// set hint to translate nodes -$query->setHint( - Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, - 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker' -); -$treeDecorationOptions = array( - 'decorate' => true, - 'rootOpen' => '', - 'rootClose' => '', - 'childOpen' => '', - 'childClose' => '', - 'nodeDecorator' => function ($node) { - return str_repeat('-', $node['level']).$node['title'].PHP_EOL; - }, -); -// build tree in english -echo $repository->buildTree($query->getArrayResult(), $treeDecorationOptions).PHP_EOL.PHP_EOL; -// change locale -$query->setHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'lt'); -// build tree in lithuanian -echo $repository->buildTree($query->getArrayResult(), $treeDecorationOptions).PHP_EOL.PHP_EOL; - -$ms = round(microtime(true) - $executionStart, 4) * 1000; -$mem = round((memory_get_usage(true) - $memoryStart) / 1000000, 2); -echo "Execution took: {$ms} ms, memory consumed: {$mem} Mb"; diff --git a/lib/Gedmo/Blameable/Blameable.php b/lib/Gedmo/Blameable/Blameable.php deleted file mode 100644 index 251b02806f..0000000000 --- a/lib/Gedmo/Blameable/Blameable.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface Blameable -{ - // blameable expects annotations on properties - - /** - * @gedmo:Blameable(on="create") - * fields which should be updated on insert only - */ - - /** - * @gedmo:Blameable(on="update") - * fields which should be updated on update and insert - */ - - /** - * @gedmo:Blameable(on="change", field="field", value="value") - * fields which should be updated on changed "property" - * value and become equal to given "value" - */ - - /** - * @gedmo:Blameable(on="change", field="field") - * fields which should be updated on changed "property" - */ - - /** - * @gedmo:Blameable(on="change", fields={"field1", "field2"}) - * fields which should be updated if at least one of the given fields changed - */ - - /** - * example - * - * @gedmo:Blameable(on="create") - * @Column(type="string") - * $created - */ -} diff --git a/lib/Gedmo/Blameable/BlameableListener.php b/lib/Gedmo/Blameable/BlameableListener.php deleted file mode 100644 index 6cdd09eabe..0000000000 --- a/lib/Gedmo/Blameable/BlameableListener.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class BlameableListener extends AbstractTrackingListener -{ - protected $user; - - /** - * Get the user value to set on a blameable field - * - * @param object $meta - * @param string $field - * - * @return mixed - */ - public function getFieldValue($meta, $field, $eventAdapter) - { - if ($meta->hasAssociation($field)) { - if (null !== $this->user && ! is_object($this->user)) { - throw new InvalidArgumentException("Blame is reference, user must be an object"); - } - - return $this->user; - } - - // ok so its not an association, then it is a string - if (is_object($this->user)) { - if (method_exists($this->user, 'getUsername')) { - return (string) $this->user->getUsername(); - } - if (method_exists($this->user, '__toString')) { - return $this->user->__toString(); - } - throw new InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString"); - } - - return $this->user; - } - - /** - * Set a user value to return - * - * @param mixed $user - */ - public function setUserValue($user) - { - $this->user = $user; - } - - /** - * {@inheritDoc} - */ - protected function getNamespace() - { - return __NAMESPACE__; - } -} diff --git a/lib/Gedmo/Blameable/Mapping/Event/BlameableAdapter.php b/lib/Gedmo/Blameable/Mapping/Event/BlameableAdapter.php deleted file mode 100644 index 5bed21d172..0000000000 --- a/lib/Gedmo/Blameable/Mapping/Event/BlameableAdapter.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface BlameableAdapter extends AdapterInterface -{ -} diff --git a/lib/Gedmo/DoctrineExtensions.php b/lib/Gedmo/DoctrineExtensions.php deleted file mode 100644 index 7654474880..0000000000 --- a/lib/Gedmo/DoctrineExtensions.php +++ /dev/null @@ -1,117 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class DoctrineExtensions -{ - /** - * Current version of extensions - */ - const VERSION = 'v2.4.9'; - - /** - * Hooks all extensions metadata mapping drivers - * into given $driverChain of drivers for ORM - * - * @param MappingDriverChain $driverChain - * @param Reader|null $reader - */ - public static function registerMappingIntoDriverChainORM(MappingDriverChain $driverChain, Reader $reader = null) - { - self::registerAnnotations(); - if (!$reader) { - $reader = new CachedReader(new AnnotationReader(), new ArrayCache()); - } - $annotationDriver = new DriverORM\AnnotationDriver($reader, array( - __DIR__.'/Translatable/Entity', - __DIR__.'/Loggable/Entity', - __DIR__.'/Tree/Entity', - )); - $driverChain->addDriver($annotationDriver, 'Gedmo'); - } - - /** - * Hooks only superclass metadata mapping drivers - * into given $driverChain of drivers for ORM - * - * @param MappingDriverChain $driverChain - * @param Reader|null $reader - */ - public static function registerAbstractMappingIntoDriverChainORM(MappingDriverChain $driverChain, Reader $reader = null) - { - self::registerAnnotations(); - if (!$reader) { - $reader = new CachedReader(new AnnotationReader(), new ArrayCache()); - } - $annotationDriver = new DriverORM\AnnotationDriver($reader, array( - __DIR__.'/Translatable/Entity/MappedSuperclass', - __DIR__.'/Loggable/Entity/MappedSuperclass', - __DIR__.'/Tree/Entity/MappedSuperclass', - )); - $driverChain->addDriver($annotationDriver, 'Gedmo'); - } - - /** - * Hooks all extensions metadata mapping drivers - * into given $driverChain of drivers for ODM MongoDB - * - * @param MappingDriverChain $driverChain - * @param Reader|null $reader - */ - public static function registerMappingIntoDriverChainMongodbODM(MappingDriverChain $driverChain, Reader $reader = null) - { - self::registerAnnotations(); - if (!$reader) { - $reader = new CachedReader(new AnnotationReader(), new ArrayCache()); - } - $annotationDriver = new DriverMongodbODM\AnnotationDriver($reader, array( - __DIR__.'/Translatable/Document', - __DIR__.'/Loggable/Document', - )); - $driverChain->addDriver($annotationDriver, 'Gedmo'); - } - - /** - * Hooks only superclass metadata mapping drivers - * into given $driverChain of drivers for ODM MongoDB - * - * @param MappingDriverChain $driverChain - * @param Reader|null $reader - */ - public static function registerAbstractMappingIntoDriverChainMongodbODM(MappingDriverChain $driverChain, Reader $reader = null) - { - self::registerAnnotations(); - if (!$reader) { - $reader = new CachedReader(new AnnotationReader(), new ArrayCache()); - } - $annotationDriver = new DriverMongodbODM\AnnotationDriver($reader, array( - __DIR__.'/Translatable/Document/MappedSuperclass', - __DIR__.'/Loggable/Document/MappedSuperclass', - )); - $driverChain->addDriver($annotationDriver, 'Gedmo'); - } - - /** - * Includes all extension annotations once - */ - public static function registerAnnotations() - { - AnnotationRegistry::registerFile(__DIR__.'/Mapping/Annotation/All.php'); - } -} diff --git a/lib/Gedmo/Exception.php b/lib/Gedmo/Exception.php deleted file mode 100644 index fcbe45b40f..0000000000 --- a/lib/Gedmo/Exception.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface Exception -{ - /** - * Following best practices for PHP5.3 package exceptions. - * All exceptions thrown in this package will have to implement this interface - */ -} diff --git a/lib/Gedmo/Exception/BadMethodCallException.php b/lib/Gedmo/Exception/BadMethodCallException.php deleted file mode 100644 index 55d6a2c660..0000000000 --- a/lib/Gedmo/Exception/BadMethodCallException.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class BadMethodCallException - extends \BadMethodCallException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/FeatureNotImplementedException.php b/lib/Gedmo/Exception/FeatureNotImplementedException.php deleted file mode 100644 index 46f235edf4..0000000000 --- a/lib/Gedmo/Exception/FeatureNotImplementedException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class FeatureNotImplementedException - extends \RuntimeException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/InvalidArgumentException.php b/lib/Gedmo/Exception/InvalidArgumentException.php deleted file mode 100644 index 622f1aba8b..0000000000 --- a/lib/Gedmo/Exception/InvalidArgumentException.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class InvalidArgumentException - extends \InvalidArgumentException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/InvalidMappingException.php b/lib/Gedmo/Exception/InvalidMappingException.php deleted file mode 100644 index e43fe4c4a2..0000000000 --- a/lib/Gedmo/Exception/InvalidMappingException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class InvalidMappingException - extends InvalidArgumentException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/ReferenceIntegrityStrictException.php b/lib/Gedmo/Exception/ReferenceIntegrityStrictException.php deleted file mode 100644 index e436e3f5f5..0000000000 --- a/lib/Gedmo/Exception/ReferenceIntegrityStrictException.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class ReferenceIntegrityStrictException extends RuntimeException -{ -} diff --git a/lib/Gedmo/Exception/RuntimeException.php b/lib/Gedmo/Exception/RuntimeException.php deleted file mode 100644 index 4f83b41ebb..0000000000 --- a/lib/Gedmo/Exception/RuntimeException.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class RuntimeException - extends \RuntimeException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/TreeLockingException.php b/lib/Gedmo/Exception/TreeLockingException.php deleted file mode 100644 index 53ffa20405..0000000000 --- a/lib/Gedmo/Exception/TreeLockingException.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class TreeLockingException extends RuntimeException -{ -} diff --git a/lib/Gedmo/Exception/UnexpectedValueException.php b/lib/Gedmo/Exception/UnexpectedValueException.php deleted file mode 100644 index c1d95140c4..0000000000 --- a/lib/Gedmo/Exception/UnexpectedValueException.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UnexpectedValueException - extends \UnexpectedValueException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UnsupportedObjectManagerException.php b/lib/Gedmo/Exception/UnsupportedObjectManagerException.php deleted file mode 100644 index 8acf66cc7a..0000000000 --- a/lib/Gedmo/Exception/UnsupportedObjectManagerException.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UnsupportedObjectManagerException - extends InvalidArgumentException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableCantWriteException.php b/lib/Gedmo/Exception/UploadableCantWriteException.php deleted file mode 100644 index e74ad8ddb4..0000000000 --- a/lib/Gedmo/Exception/UploadableCantWriteException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableCantWriteException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableCouldntGuessMimeTypeException.php b/lib/Gedmo/Exception/UploadableCouldntGuessMimeTypeException.php deleted file mode 100644 index 7b25b69f9f..0000000000 --- a/lib/Gedmo/Exception/UploadableCouldntGuessMimeTypeException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableCouldntGuessMimeTypeException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableDirectoryNotFoundException.php b/lib/Gedmo/Exception/UploadableDirectoryNotFoundException.php deleted file mode 100644 index 1d75c38cd7..0000000000 --- a/lib/Gedmo/Exception/UploadableDirectoryNotFoundException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableDirectoryNotFoundException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableException.php b/lib/Gedmo/Exception/UploadableException.php deleted file mode 100644 index 2639f08ae7..0000000000 --- a/lib/Gedmo/Exception/UploadableException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableException - extends RuntimeException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableExtensionException.php b/lib/Gedmo/Exception/UploadableExtensionException.php deleted file mode 100644 index f53bd53e32..0000000000 --- a/lib/Gedmo/Exception/UploadableExtensionException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableExtensionException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableFileAlreadyExistsException.php b/lib/Gedmo/Exception/UploadableFileAlreadyExistsException.php deleted file mode 100644 index fdc80eb1e1..0000000000 --- a/lib/Gedmo/Exception/UploadableFileAlreadyExistsException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableFileAlreadyExistsException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableFileNotReadableException.php b/lib/Gedmo/Exception/UploadableFileNotReadableException.php deleted file mode 100644 index f537e7c777..0000000000 --- a/lib/Gedmo/Exception/UploadableFileNotReadableException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableFileNotReadableException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableFormSizeException.php b/lib/Gedmo/Exception/UploadableFormSizeException.php deleted file mode 100644 index 6b76153000..0000000000 --- a/lib/Gedmo/Exception/UploadableFormSizeException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableFormSizeException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableIniSizeException.php b/lib/Gedmo/Exception/UploadableIniSizeException.php deleted file mode 100644 index b56923ee30..0000000000 --- a/lib/Gedmo/Exception/UploadableIniSizeException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableIniSizeException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableInvalidFileException.php b/lib/Gedmo/Exception/UploadableInvalidFileException.php deleted file mode 100644 index 9dd222ac10..0000000000 --- a/lib/Gedmo/Exception/UploadableInvalidFileException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableInvalidFileException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableInvalidMimeTypeException.php b/lib/Gedmo/Exception/UploadableInvalidMimeTypeException.php deleted file mode 100644 index 13080ff1a2..0000000000 --- a/lib/Gedmo/Exception/UploadableInvalidMimeTypeException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableInvalidMimeTypeException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableInvalidPathException.php b/lib/Gedmo/Exception/UploadableInvalidPathException.php deleted file mode 100644 index 793213a15b..0000000000 --- a/lib/Gedmo/Exception/UploadableInvalidPathException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableInvalidPathException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableMaxSizeException.php b/lib/Gedmo/Exception/UploadableMaxSizeException.php deleted file mode 100644 index 5f0bd14b74..0000000000 --- a/lib/Gedmo/Exception/UploadableMaxSizeException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableMaxSizeException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableNoFileException.php b/lib/Gedmo/Exception/UploadableNoFileException.php deleted file mode 100644 index cb33c32529..0000000000 --- a/lib/Gedmo/Exception/UploadableNoFileException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableNoFileException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableNoPathDefinedException.php b/lib/Gedmo/Exception/UploadableNoPathDefinedException.php deleted file mode 100644 index 7d33965153..0000000000 --- a/lib/Gedmo/Exception/UploadableNoPathDefinedException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableNoPathDefinedException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableNoTmpDirException.php b/lib/Gedmo/Exception/UploadableNoTmpDirException.php deleted file mode 100644 index 3d02477ad5..0000000000 --- a/lib/Gedmo/Exception/UploadableNoTmpDirException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableNoTmpDirException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadablePartialException.php b/lib/Gedmo/Exception/UploadablePartialException.php deleted file mode 100644 index cc769b1f02..0000000000 --- a/lib/Gedmo/Exception/UploadablePartialException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadablePartialException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/Exception/UploadableUploadException.php b/lib/Gedmo/Exception/UploadableUploadException.php deleted file mode 100644 index 32022bae14..0000000000 --- a/lib/Gedmo/Exception/UploadableUploadException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableUploadException - extends UploadableException - implements Exception -{ -} diff --git a/lib/Gedmo/IpTraceable/IpTraceable.php b/lib/Gedmo/IpTraceable/IpTraceable.php deleted file mode 100644 index a24b092169..0000000000 --- a/lib/Gedmo/IpTraceable/IpTraceable.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface IpTraceable -{ - // ipTraceable expects annotations on properties - - /** - * @gedmo:IpTraceable(on="create") - * strings which should be updated on insert only - */ - - /** - * @gedmo:IpTraceable(on="update") - * strings which should be updated on update and insert - */ - - /** - * @gedmo:IpTraceable(on="change", field="field", value="value") - * strings which should be updated on changed "property" - * value and become equal to given "value" - */ - - /** - * @gedmo:IpTraceable(on="change", field="field") - * strings which should be updated on changed "property" - */ - - /** - * @gedmo:IpTraceable(on="change", fields={"field1", "field2"}) - * strings which should be updated if at least one of the given fields changed - */ - - /** - * example - * - * @gedmo:IpTraceable(on="create") - * @Column(type="string") - * $created - */ -} diff --git a/lib/Gedmo/IpTraceable/IpTraceableListener.php b/lib/Gedmo/IpTraceable/IpTraceableListener.php deleted file mode 100644 index b09cda5c58..0000000000 --- a/lib/Gedmo/IpTraceable/IpTraceableListener.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class IpTraceableListener extends AbstractTrackingListener -{ - /** - * @var string|null - */ - protected $ip; - - /** - * Get the ipValue value to set on a ip field - * - * @param object $meta - * @param string $field - * @param AdapterInterface $eventAdapter - * - * @return null|string - */ - public function getFieldValue($meta, $field, $eventAdapter) - { - return $this->ip; - } - - /** - * Set a ip value to return - * - * @param string $ip - * @throws InvalidArgumentException - */ - public function setIpValue($ip = null) - { - if (isset($ip) && filter_var($ip, FILTER_VALIDATE_IP) === false) { - throw new InvalidArgumentException("ip address is not valid $ip"); - } - - $this->ip = $ip; - } - - /** - * {@inheritDoc} - */ - protected function getNamespace() - { - return __NAMESPACE__; - } -} diff --git a/lib/Gedmo/IpTraceable/Mapping/Event/IpTraceableAdapter.php b/lib/Gedmo/IpTraceable/Mapping/Event/IpTraceableAdapter.php deleted file mode 100644 index 9128a35a56..0000000000 --- a/lib/Gedmo/IpTraceable/Mapping/Event/IpTraceableAdapter.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface IpTraceableAdapter extends AdapterInterface -{ -} diff --git a/lib/Gedmo/Loggable/Document/LogEntry.php b/lib/Gedmo/Loggable/Document/LogEntry.php deleted file mode 100644 index 8309baf25d..0000000000 --- a/lib/Gedmo/Loggable/Document/LogEntry.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class LogEntryRepository extends EntityRepository -{ - /** - * Currently used loggable listener - * - * @var LoggableListener - */ - private $listener; - - /** - * Loads all log entries for the given entity - * - * @param object $entity - * - * @return LogEntry[] - */ - public function getLogEntries($entity) - { - $q = $this->getLogEntriesQuery($entity); - - return $q->getResult(); - } - - /** - * Get the query for loading of log entries - * - * @param object $entity - * - * @return Query - */ - public function getLogEntriesQuery($entity) - { - $wrapped = new EntityWrapper($entity, $this->_em); - $objectClass = $wrapped->getMetadata()->name; - $meta = $this->getClassMetadata(); - $dql = "SELECT log FROM {$meta->name} log"; - $dql .= " WHERE log.objectId = :objectId"; - $dql .= " AND log.objectClass = :objectClass"; - $dql .= " ORDER BY log.version DESC"; - - $objectId = $wrapped->getIdentifier(); - $q = $this->_em->createQuery($dql); - $q->setParameters(compact('objectId', 'objectClass')); - - return $q; - } - - /** - * Reverts given $entity to $revision by - * restoring all fields from that $revision. - * After this operation you will need to - * persist and flush the $entity. - * - * @param object $entity - * @param integer $version - * - * @throws \Gedmo\Exception\UnexpectedValueException - * - * @return void - */ - public function revert($entity, $version = 1) - { - $wrapped = new EntityWrapper($entity, $this->_em); - $objectMeta = $wrapped->getMetadata(); - $objectClass = $objectMeta->name; - $meta = $this->getClassMetadata(); - $dql = "SELECT log FROM {$meta->name} log"; - $dql .= " WHERE log.objectId = :objectId"; - $dql .= " AND log.objectClass = :objectClass"; - $dql .= " AND log.version <= :version"; - $dql .= " ORDER BY log.version ASC"; - - $objectId = $wrapped->getIdentifier(); - $q = $this->_em->createQuery($dql); - $q->setParameters(compact('objectId', 'objectClass', 'version')); - $logs = $q->getResult(); - - if ($logs) { - $config = $this->getLoggableListener()->getConfiguration($this->_em, $objectMeta->name); - $fields = $config['versioned']; - $filled = false; - while (($log = array_pop($logs)) && !$filled) { - if ($data = $log->getData()) { - foreach ($data as $field => $value) { - if (in_array($field, $fields)) { - $this->mapValue($objectMeta, $field, $value); - $wrapped->setPropertyValue($field, $value); - unset($fields[array_search($field, $fields)]); - } - } - } - $filled = count($fields) === 0; - } - /*if (count($fields)) { - throw new \Gedmo\Exception\UnexpectedValueException('Could not fully revert the entity to version: '.$version); - }*/ - } else { - throw new \Gedmo\Exception\UnexpectedValueException('Could not find any log entries under version: '.$version); - } - } - - /** - * @param ClassMetadata $objectMeta - * @param string $field - * @param mixed $value - */ - protected function mapValue(ClassMetadata $objectMeta, $field, &$value) - { - if (!$objectMeta->isSingleValuedAssociation($field)) { - return; - } - - $mapping = $objectMeta->getAssociationMapping($field); - $value = $value ? $this->_em->getReference($mapping['targetEntity'], $value) : null; - } - - /** - * Get the currently used LoggableListener - * - * @throws \Gedmo\Exception\RuntimeException - if listener is not found - * - * @return LoggableListener - */ - private function getLoggableListener() - { - if (is_null($this->listener)) { - foreach ($this->_em->getEventManager()->getListeners() as $event => $listeners) { - foreach ($listeners as $hash => $listener) { - if ($listener instanceof LoggableListener) { - $this->listener = $listener; - break; - } - } - if ($this->listener) { - break; - } - } - - if (is_null($this->listener)) { - throw new \Gedmo\Exception\RuntimeException('The loggable listener could not be found'); - } - } - - return $this->listener; - } -} diff --git a/lib/Gedmo/Loggable/Loggable.php b/lib/Gedmo/Loggable/Loggable.php deleted file mode 100644 index 9adb9a9162..0000000000 --- a/lib/Gedmo/Loggable/Loggable.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface Loggable -{ - // this interface is not necessary to implement - - /** - * @gedmo:Loggable - * to mark the class as loggable use class annotation @gedmo:Loggable - * this object will contain now a history - * available options: - * logEntryClass="My\LogEntryObject" (optional) defaultly will use internal object class - * example: - * - * @gedmo:Loggable(logEntryClass="My\LogEntryObject") - * class MyEntity - */ -} diff --git a/lib/Gedmo/Loggable/LoggableListener.php b/lib/Gedmo/Loggable/LoggableListener.php deleted file mode 100644 index 7621850c90..0000000000 --- a/lib/Gedmo/Loggable/LoggableListener.php +++ /dev/null @@ -1,324 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class LoggableListener extends MappedEventSubscriber -{ - /** - * Create action - */ - const ACTION_CREATE = 'create'; - - /** - * Update action - */ - const ACTION_UPDATE = 'update'; - - /** - * Remove action - */ - const ACTION_REMOVE = 'remove'; - - /** - * Username for identification - * - * @var string - */ - protected $username; - - /** - * List of log entries which do not have the foreign - * key generated yet - MySQL case. These entries - * will be updated with new keys on postPersist event - * - * @var array - */ - protected $pendingLogEntryInserts = array(); - - /** - * For log of changed relations we use - * its identifiers to avoid storing serialized Proxies. - * These are pending relations in case it does not - * have an identifier yet - * - * @var array - */ - protected $pendingRelatedObjects = array(); - - /** - * Set username for identification - * - * @param mixed $username - * - * @throws \Gedmo\Exception\InvalidArgumentException Invalid username - */ - public function setUsername($username) - { - if (is_string($username)) { - $this->username = $username; - } elseif (is_object($username) && method_exists($username, 'getUsername')) { - $this->username = (string) $username->getUsername(); - } else { - throw new \Gedmo\Exception\InvalidArgumentException("Username must be a string, or object should have method: getUsername"); - } - } - - /** - * {@inheritdoc} - */ - public function getSubscribedEvents() - { - return array( - 'onFlush', - 'loadClassMetadata', - 'postPersist', - ); - } - - /** - * Get the LogEntry class - * - * @param LoggableAdapter $ea - * @param string $class - * - * @return string - */ - protected function getLogEntryClass(LoggableAdapter $ea, $class) - { - return isset(self::$configurations[$this->name][$class]['logEntryClass']) ? - self::$configurations[$this->name][$class]['logEntryClass'] : - $ea->getDefaultLogEntryClass(); - } - - /** - * Maps additional metadata - * - * @param EventArgs $eventArgs - * - * @return void - */ - public function loadClassMetadata(EventArgs $eventArgs) - { - $ea = $this->getEventAdapter($eventArgs); - $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata()); - } - - /** - * Checks for inserted object to update its logEntry - * foreign key - * - * @param EventArgs $args - * - * @return void - */ - public function postPersist(EventArgs $args) - { - $ea = $this->getEventAdapter($args); - $object = $ea->getObject(); - $om = $ea->getObjectManager(); - $oid = spl_object_hash($object); - $uow = $om->getUnitOfWork(); - if ($this->pendingLogEntryInserts && array_key_exists($oid, $this->pendingLogEntryInserts)) { - $wrapped = AbstractWrapper::wrap($object, $om); - - $logEntry = $this->pendingLogEntryInserts[$oid]; - $logEntryMeta = $om->getClassMetadata(get_class($logEntry)); - - $id = $wrapped->getIdentifier(); - $logEntryMeta->getReflectionProperty('objectId')->setValue($logEntry, $id); - $uow->scheduleExtraUpdate($logEntry, array( - 'objectId' => array(null, $id), - )); - $ea->setOriginalObjectProperty($uow, spl_object_hash($logEntry), 'objectId', $id); - unset($this->pendingLogEntryInserts[$oid]); - } - if ($this->pendingRelatedObjects && array_key_exists($oid, $this->pendingRelatedObjects)) { - $wrapped = AbstractWrapper::wrap($object, $om); - $identifiers = $wrapped->getIdentifier(false); - foreach ($this->pendingRelatedObjects[$oid] as $props) { - $logEntry = $props['log']; - $logEntryMeta = $om->getClassMetadata(get_class($logEntry)); - $oldData = $data = $logEntry->getData(); - $data[$props['field']] = $identifiers; - - $logEntry->setData($data); - - $uow->scheduleExtraUpdate($logEntry, array( - 'data' => array($oldData, $data), - )); - $ea->setOriginalObjectProperty($uow, spl_object_hash($logEntry), 'data', $data); - } - unset($this->pendingRelatedObjects[$oid]); - } - } - - /** - * Handle any custom LogEntry functionality that needs to be performed - * before persisting it - * - * @param object $logEntry The LogEntry being persisted - * @param object $object The object being Logged - */ - protected function prePersistLogEntry($logEntry, $object) - { - - } - - /** - * Looks for loggable objects being inserted or updated - * for further processing - * - * @param EventArgs $eventArgs - * - * @return void - */ - public function onFlush(EventArgs $eventArgs) - { - $ea = $this->getEventAdapter($eventArgs); - $om = $ea->getObjectManager(); - $uow = $om->getUnitOfWork(); - - foreach ($ea->getScheduledObjectInsertions($uow) as $object) { - $this->createLogEntry(self::ACTION_CREATE, $object, $ea); - } - foreach ($ea->getScheduledObjectUpdates($uow) as $object) { - $this->createLogEntry(self::ACTION_UPDATE, $object, $ea); - } - foreach ($ea->getScheduledObjectDeletions($uow) as $object) { - $this->createLogEntry(self::ACTION_REMOVE, $object, $ea); - } - } - - /** - * {@inheritDoc} - */ - protected function getNamespace() - { - return __NAMESPACE__; - } - - /** - * Returns an objects changeset data - * - * @param LoggableAdapter $ea - * @param object $object - * @param object $logEntry - * - * @return array - */ - protected function getObjectChangeSetData($ea, $object, $logEntry) - { - $om = $ea->getObjectManager(); - $wrapped = AbstractWrapper::wrap($object, $om); - $meta = $wrapped->getMetadata(); - $config = $this->getConfiguration($om, $meta->name); - $uow = $om->getUnitOfWork(); - $newValues = array(); - - foreach ($ea->getObjectChangeSet($uow, $object) as $field => $changes) { - if (empty($config['versioned']) || !in_array($field, $config['versioned'])) { - continue; - } - $value = $changes[1]; - if ($meta->isSingleValuedAssociation($field) && $value) { - if ($wrapped->isEmbeddedAssociation($field)) { - $value = $this->getObjectChangeSetData($ea, $value, $logEntry); - } else { - $oid = spl_object_hash($value); - $wrappedAssoc = AbstractWrapper::wrap($value, $om); - $value = $wrappedAssoc->getIdentifier(false); - if (!is_array($value) && !$value) { - $this->pendingRelatedObjects[$oid][] = array( - 'log' => $logEntry, - 'field' => $field, - ); - } - } - } - $newValues[$field] = $value; - } - - return $newValues; - } - - /** - * Create a new Log instance - * - * @param string $action - * @param object $object - * @param LoggableAdapter $ea - * - * @return \Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry|null - */ - protected function createLogEntry($action, $object, LoggableAdapter $ea) - { - $om = $ea->getObjectManager(); - $wrapped = AbstractWrapper::wrap($object, $om); - $meta = $wrapped->getMetadata(); - - // Filter embedded documents - if (isset($meta->isEmbeddedDocument) && $meta->isEmbeddedDocument) { - return; - } - - if ($config = $this->getConfiguration($om, $meta->name)) { - $logEntryClass = $this->getLogEntryClass($ea, $meta->name); - $logEntryMeta = $om->getClassMetadata($logEntryClass); - /** @var \Gedmo\Loggable\Entity\LogEntry $logEntry */ - $logEntry = $logEntryMeta->newInstance(); - - $logEntry->setAction($action); - $logEntry->setUsername($this->username); - $logEntry->setObjectClass($meta->name); - $logEntry->setLoggedAt(); - - // check for the availability of the primary key - $uow = $om->getUnitOfWork(); - if ($action === self::ACTION_CREATE && $ea->isPostInsertGenerator($meta)) { - $this->pendingLogEntryInserts[spl_object_hash($object)] = $logEntry; - } else { - $logEntry->setObjectId($wrapped->getIdentifier()); - } - $newValues = array(); - if ($action !== self::ACTION_REMOVE && isset($config['versioned'])) { - $newValues = $this->getObjectChangeSetData($ea, $object, $logEntry); - $logEntry->setData($newValues); - } - - if($action === self::ACTION_UPDATE && 0 === count($newValues)) { - return null; - } - - $version = 1; - if ($action !== self::ACTION_CREATE) { - $version = $ea->getNewVersion($logEntryMeta, $object); - if (empty($version)) { - // was versioned later - $version = 1; - } - } - $logEntry->setVersion($version); - - $this->prePersistLogEntry($logEntry, $object); - - $om->persist($logEntry); - $uow->computeChangeSet($logEntryMeta, $logEntry); - - return $logEntry; - } - - return null; - } -} diff --git a/lib/Gedmo/Loggable/Mapping/Driver/Annotation.php b/lib/Gedmo/Loggable/Mapping/Driver/Annotation.php deleted file mode 100644 index e45f31bc82..0000000000 --- a/lib/Gedmo/Loggable/Mapping/Driver/Annotation.php +++ /dev/null @@ -1,136 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Annotation extends AbstractAnnotationDriver -{ - /** - * Annotation to define that this object is loggable - */ - const LOGGABLE = 'Gedmo\\Mapping\\Annotation\\Loggable'; - - /** - * Annotation to define that this property is versioned - */ - const VERSIONED = 'Gedmo\\Mapping\\Annotation\\Versioned'; - - /** - * {@inheritDoc} - */ - public function validateFullMetadata(ClassMetadata $meta, array $config) - { - if ($config && is_array($meta->identifier) && count($meta->identifier) > 1) { - throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->name}"); - } - if (isset($config['versioned']) && !isset($config['loggable'])) { - throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->name}"); - } - } - - /** - * {@inheritDoc} - */ - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - // class annotations - if ($annot = $this->reader->getClassAnnotation($class, self::LOGGABLE)) { - $config['loggable'] = true; - if ($annot->logEntryClass) { - if (!$cl = $this->getRelatedClassName($meta, $annot->logEntryClass)) { - throw new InvalidMappingException("LogEntry class: {$annot->logEntryClass} does not exist."); - } - $config['logEntryClass'] = $cl; - } - } - - // property annotations - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() || - $meta->isInheritedField($property->name) || - isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - - // versioned property - if ($this->reader->getPropertyAnnotation($property, self::VERSIONED)) { - $field = $property->getName(); - if (!$this->isMappingValid($meta, $field)) { - throw new InvalidMappingException("Cannot versioned [{$field}] as it is collection in object - {$meta->name}"); - } - if (isset($meta->embeddedClasses[$field])) { - $this->inspectEmbeddedForVersioned($field, $config, $meta); - continue; - } - // fields cannot be overrided and throws mapping exception - $config['versioned'][] = $field; - } - } - - if (!$meta->isMappedSuperclass && $config) { - if (is_array($meta->identifier) && count($meta->identifier) > 1) { - throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->name}"); - } - if ($this->isClassAnnotationInValid($meta, $config)) { - throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->name}"); - } - } - } - - /** - * @param ClassMetadata $meta - * @param string $field - * - * @return bool - */ - protected function isMappingValid(ClassMetadata $meta, $field) - { - return $meta->isCollectionValuedAssociation($field) == false; - } - - /** - * @param ClassMetadata $meta - * @param array $config - * - * @return bool - */ - protected function isClassAnnotationInValid(ClassMetadata $meta, array &$config) - { - return isset($config['versioned']) && !isset($config['loggable']) && (!isset($meta->isEmbeddedClass) || !$meta->isEmbeddedClass); - } - - /** - * Searches properties of embedded object for versioned fields - * - * @param string $field - * @param array $config - * @param \Doctrine\ORM\Mapping\ClassMetadata $meta - */ - private function inspectEmbeddedForVersioned($field, array &$config, \Doctrine\ORM\Mapping\ClassMetadata $meta) - { - $ัlass = new \ReflectionClass($meta->embeddedClasses[$field]['class']); - - // property annotations - foreach ($ัlass->getProperties() as $property) { - // versioned property - if ($this->reader->getPropertyAnnotation($property, self::VERSIONED)) { - $config['versioned'][] = $field . '.' . $property->getName(); - } - } - } -} diff --git a/lib/Gedmo/Loggable/Mapping/Event/Adapter/ORM.php b/lib/Gedmo/Loggable/Mapping/Event/Adapter/ORM.php deleted file mode 100644 index ec87f8d6f4..0000000000 --- a/lib/Gedmo/Loggable/Mapping/Event/Adapter/ORM.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class ORM extends BaseAdapterORM implements LoggableAdapter -{ - /** - * {@inheritDoc} - */ - public function getDefaultLogEntryClass() - { - return 'Gedmo\\Loggable\\Entity\\LogEntry'; - } - - /** - * {@inheritDoc} - */ - public function isPostInsertGenerator($meta) - { - return $meta->idGenerator->isPostInsertGenerator(); - } - - /** - * {@inheritDoc} - */ - public function getNewVersion($meta, $object) - { - $em = $this->getObjectManager(); - $objectMeta = $em->getClassMetadata(get_class($object)); - $identifierField = $this->getSingleIdentifierFieldName($objectMeta); - $objectId = $objectMeta->getReflectionProperty($identifierField)->getValue($object); - - $dql = "SELECT MAX(log.version) FROM {$meta->name} log"; - $dql .= " WHERE log.objectId = :objectId"; - $dql .= " AND log.objectClass = :objectClass"; - - $q = $em->createQuery($dql); - $q->setParameters(array( - 'objectId' => $objectId, - 'objectClass' => $objectMeta->name, - )); - - return $q->getSingleScalarResult() + 1; - } -} diff --git a/lib/Gedmo/Loggable/Mapping/Event/LoggableAdapter.php b/lib/Gedmo/Loggable/Mapping/Event/LoggableAdapter.php deleted file mode 100644 index ba5ce47e51..0000000000 --- a/lib/Gedmo/Loggable/Mapping/Event/LoggableAdapter.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface LoggableAdapter extends AdapterInterface -{ - /** - * Get default LogEntry class used to store the logs - * - * @return string - */ - public function getDefaultLogEntryClass(); - - /** - * Checks whether an id should be generated post insert - * - * @return boolean - */ - public function isPostInsertGenerator($meta); - - /** - * Get new version number - * - * @param object $meta - * @param object $object - * - * @return integer - */ - public function getNewVersion($meta, $object); -} diff --git a/lib/Gedmo/Mapping/Annotation/All.php b/lib/Gedmo/Mapping/Annotation/All.php deleted file mode 100644 index 1df469f0f6..0000000000 --- a/lib/Gedmo/Mapping/Annotation/All.php +++ /dev/null @@ -1,15 +0,0 @@ - -* @license MIT License (http://www.opensource.org/licenses/mit-license.php) -*/ -foreach (glob(__DIR__ . "/*.php") as $filename) { - if (basename($filename, '.php') === 'All') { - continue; - } - include_once $filename; -} diff --git a/lib/Gedmo/Mapping/Annotation/Blameable.php b/lib/Gedmo/Mapping/Annotation/Blameable.php deleted file mode 100644 index 29937858a1..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Blameable.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class Blameable extends Annotation -{ - /** @var string */ - public $on = 'update'; - /** @var string|array */ - public $field; - /** @var mixed */ - public $value; -} diff --git a/lib/Gedmo/Mapping/Annotation/IpTraceable.php b/lib/Gedmo/Mapping/Annotation/IpTraceable.php deleted file mode 100644 index 7ee1892425..0000000000 --- a/lib/Gedmo/Mapping/Annotation/IpTraceable.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class IpTraceable extends Annotation -{ - /** @var string */ - public $on = 'update'; - /** @var string|array */ - public $field; - /** @var mixed */ - public $value; -} diff --git a/lib/Gedmo/Mapping/Annotation/Language.php b/lib/Gedmo/Mapping/Annotation/Language.php deleted file mode 100644 index a026a987bf..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Language.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class Language extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/Locale.php b/lib/Gedmo/Mapping/Annotation/Locale.php deleted file mode 100644 index f00602ffe8..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Locale.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class Locale extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/Loggable.php b/lib/Gedmo/Mapping/Annotation/Loggable.php deleted file mode 100644 index dbde423f62..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Loggable.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class Loggable extends Annotation -{ - /** @var string */ - public $logEntryClass; -} diff --git a/lib/Gedmo/Mapping/Annotation/Reference.php b/lib/Gedmo/Mapping/Annotation/Reference.php deleted file mode 100644 index 11dd2cb5bb..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Reference.php +++ /dev/null @@ -1,22 +0,0 @@ - ODM references extension - * to be user like "@ReferenceMany(type="entity", class="MyEntity", identifier="entity_id")" - * - * @author Bulat Shakirzyanov - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - * @Annotation - */ -abstract class Reference extends Annotation -{ - public $type; - public $class; - public $identifier; - public $mappedBy; - public $inversedBy; -} diff --git a/lib/Gedmo/Mapping/Annotation/ReferenceIntegrity.php b/lib/Gedmo/Mapping/Annotation/ReferenceIntegrity.php deleted file mode 100644 index 254578d615..0000000000 --- a/lib/Gedmo/Mapping/Annotation/ReferenceIntegrity.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class ReferenceIntegrity extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/ReferenceMany.php b/lib/Gedmo/Mapping/Annotation/ReferenceMany.php deleted file mode 100644 index 3eaa17c023..0000000000 --- a/lib/Gedmo/Mapping/Annotation/ReferenceMany.php +++ /dev/null @@ -1,15 +0,0 @@ - ODM references extension - * to be user like "@ReferenceMany(type="entity", class="MyEntity", identifier="entity_id")" - * - * @author Bulat Shakirzyanov - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - * @Annotation - */ -class ReferenceMany extends Reference -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/ReferenceManyEmbed.php b/lib/Gedmo/Mapping/Annotation/ReferenceManyEmbed.php deleted file mode 100644 index a432b12915..0000000000 --- a/lib/Gedmo/Mapping/Annotation/ReferenceManyEmbed.php +++ /dev/null @@ -1,10 +0,0 @@ - ODM references extension - * to be user like "@ReferenceOne(type="entity", class="MyEntity", identifier="entity_id")" - * - * @author Bulat Shakirzyanov - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - * @Annotation - */ -class ReferenceOne extends Reference -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/Slug.php b/lib/Gedmo/Mapping/Annotation/Slug.php deleted file mode 100644 index f1d99d9e33..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Slug.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class Slug extends Annotation -{ - /** @var array @Required */ - public $fields = array(); - /** @var boolean */ - public $updatable = true; - /** @var string */ - public $style = 'default'; // or "camel" - /** @var boolean */ - public $unique = true; - /** @var string */ - public $unique_base = null; - /** @var string */ - public $separator = '-'; - /** @var string */ - public $prefix = ''; - /** @var string */ - public $suffix = ''; - /** @var array */ - public $handlers = array(); - /** @var string */ - public $dateFormat = 'Y-m-d-H:i'; -} diff --git a/lib/Gedmo/Mapping/Annotation/SlugHandler.php b/lib/Gedmo/Mapping/Annotation/SlugHandler.php deleted file mode 100644 index 8ff2c070cc..0000000000 --- a/lib/Gedmo/Mapping/Annotation/SlugHandler.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class SlugHandler extends Annotation -{ - public $class = ''; - public $options = array(); -} diff --git a/lib/Gedmo/Mapping/Annotation/SlugHandlerOption.php b/lib/Gedmo/Mapping/Annotation/SlugHandlerOption.php deleted file mode 100644 index 5242ab2870..0000000000 --- a/lib/Gedmo/Mapping/Annotation/SlugHandlerOption.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class SlugHandlerOption extends Annotation -{ - public $name; - public $value; -} diff --git a/lib/Gedmo/Mapping/Annotation/SoftDeleteable.php b/lib/Gedmo/Mapping/Annotation/SoftDeleteable.php deleted file mode 100644 index 29713314e0..0000000000 --- a/lib/Gedmo/Mapping/Annotation/SoftDeleteable.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - * - * @Annotation - * @Target("CLASS") - */ -final class SoftDeleteable extends Annotation -{ - /** @var string */ - public $fieldName = 'deletedAt'; - - /** @var bool */ - public $timeAware = false; -} diff --git a/lib/Gedmo/Mapping/Annotation/SortableGroup.php b/lib/Gedmo/Mapping/Annotation/SortableGroup.php deleted file mode 100644 index 9a39811cdf..0000000000 --- a/lib/Gedmo/Mapping/Annotation/SortableGroup.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - * - * @Annotation - * @Target("PROPERTY") - */ -final class SortableGroup extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/SortablePosition.php b/lib/Gedmo/Mapping/Annotation/SortablePosition.php deleted file mode 100644 index 86fd52c1d6..0000000000 --- a/lib/Gedmo/Mapping/Annotation/SortablePosition.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - * - * @Annotation - * @Target("PROPERTY") - */ -final class SortablePosition extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/Timestampable.php b/lib/Gedmo/Mapping/Annotation/Timestampable.php deleted file mode 100644 index 91f72b96bd..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Timestampable.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class Timestampable extends Annotation -{ - /** @var string */ - public $on = 'update'; - /** @var string|array */ - public $field; - /** @var mixed */ - public $value; -} diff --git a/lib/Gedmo/Mapping/Annotation/Translatable.php b/lib/Gedmo/Mapping/Annotation/Translatable.php deleted file mode 100644 index 7cc28457b6..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Translatable.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class Translatable extends Annotation -{ - /** @var boolean */ - public $fallback; -} diff --git a/lib/Gedmo/Mapping/Annotation/TranslationEntity.php b/lib/Gedmo/Mapping/Annotation/TranslationEntity.php deleted file mode 100644 index e4887bf716..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TranslationEntity.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TranslationEntity extends Annotation -{ - /** @var string @Required */ - public $class; -} diff --git a/lib/Gedmo/Mapping/Annotation/Tree.php b/lib/Gedmo/Mapping/Annotation/Tree.php deleted file mode 100644 index b9901409ea..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Tree.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class Tree extends Annotation -{ - /** @var string */ - public $type = 'nested'; - - /** @var string */ - public $activateLocking = false; - - /** @var integer */ - public $lockingTimeout = 3; -} diff --git a/lib/Gedmo/Mapping/Annotation/TreeClosure.php b/lib/Gedmo/Mapping/Annotation/TreeClosure.php deleted file mode 100644 index 7026d84f86..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TreeClosure.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TreeClosure extends Annotation -{ - /** @var string @Required */ - public $class; -} diff --git a/lib/Gedmo/Mapping/Annotation/TreeLeft.php b/lib/Gedmo/Mapping/Annotation/TreeLeft.php deleted file mode 100644 index a8e3115a79..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TreeLeft.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TreeLeft extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/TreeLevel.php b/lib/Gedmo/Mapping/Annotation/TreeLevel.php deleted file mode 100644 index 11b291c2b8..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TreeLevel.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TreeLevel extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/TreeLockTime.php b/lib/Gedmo/Mapping/Annotation/TreeLockTime.php deleted file mode 100644 index 40fd5a7803..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TreeLockTime.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TreeLockTime extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/TreeParent.php b/lib/Gedmo/Mapping/Annotation/TreeParent.php deleted file mode 100644 index 5fd8ceb946..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TreeParent.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TreeParent extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/TreePath.php b/lib/Gedmo/Mapping/Annotation/TreePath.php deleted file mode 100644 index 143bb9f063..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TreePath.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @author Gediminas Morkevicius - * @author - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TreePath extends Annotation -{ - public $separator = ','; - - public $appendId = null; - - public $startsWithSeparator = false; - - public $endsWithSeparator = true; -} diff --git a/lib/Gedmo/Mapping/Annotation/TreePathHash.php b/lib/Gedmo/Mapping/Annotation/TreePathHash.php deleted file mode 100644 index be7c0617c2..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TreePathHash.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TreePathHash extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/TreePathSource.php b/lib/Gedmo/Mapping/Annotation/TreePathSource.php deleted file mode 100644 index 648358bf3c..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TreePathSource.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TreePathSource extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/TreeRight.php b/lib/Gedmo/Mapping/Annotation/TreeRight.php deleted file mode 100644 index f7831f650f..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TreeRight.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TreeRight extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/TreeRoot.php b/lib/Gedmo/Mapping/Annotation/TreeRoot.php deleted file mode 100644 index 4dfb1e6652..0000000000 --- a/lib/Gedmo/Mapping/Annotation/TreeRoot.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class TreeRoot extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/Uploadable.php b/lib/Gedmo/Mapping/Annotation/Uploadable.php deleted file mode 100644 index 107da1d497..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Uploadable.php +++ /dev/null @@ -1,46 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class Uploadable extends Annotation -{ - /** @var boolean */ - public $allowOverwrite = false; - - /** @var boolean */ - public $appendNumber = false; - - /** @var string */ - public $path = ''; - - /** @var string */ - public $pathMethod = ''; - - /** @var string */ - public $callback = ''; - - /** @var string */ - public $filenameGenerator = Validator::FILENAME_GENERATOR_NONE; - - /** @var double */ - public $maxSize = 0; - - /** @var array */ - public $allowedTypes = ''; - - /** @var array */ - public $disallowedTypes = ''; -} diff --git a/lib/Gedmo/Mapping/Annotation/UploadableFileMimeType.php b/lib/Gedmo/Mapping/Annotation/UploadableFileMimeType.php deleted file mode 100644 index 714f5dd427..0000000000 --- a/lib/Gedmo/Mapping/Annotation/UploadableFileMimeType.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class UploadableFileMimeType extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/UploadableFileName.php b/lib/Gedmo/Mapping/Annotation/UploadableFileName.php deleted file mode 100644 index 478d1bdc8b..0000000000 --- a/lib/Gedmo/Mapping/Annotation/UploadableFileName.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class UploadableFileName extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/UploadableFilePath.php b/lib/Gedmo/Mapping/Annotation/UploadableFilePath.php deleted file mode 100644 index 8981bab240..0000000000 --- a/lib/Gedmo/Mapping/Annotation/UploadableFilePath.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class UploadableFilePath extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/UploadableFileSize.php b/lib/Gedmo/Mapping/Annotation/UploadableFileSize.php deleted file mode 100644 index 2644771056..0000000000 --- a/lib/Gedmo/Mapping/Annotation/UploadableFileSize.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class UploadableFileSize extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Annotation/Versioned.php b/lib/Gedmo/Mapping/Annotation/Versioned.php deleted file mode 100644 index d68bee8f7e..0000000000 --- a/lib/Gedmo/Mapping/Annotation/Versioned.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class Versioned extends Annotation -{ -} diff --git a/lib/Gedmo/Mapping/Driver.php b/lib/Gedmo/Mapping/Driver.php deleted file mode 100644 index 67e4b9d2eb..0000000000 --- a/lib/Gedmo/Mapping/Driver.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface Driver -{ - /** - * Read extended metadata configuration for - * a single mapped class - * - * @param object $meta - * @param array $config - * - * @return void - */ - public function readExtendedMetadata($meta, array &$config); - - /** - * Passes in the original driver - * - * @param object $driver - * - * @return void - */ - public function setOriginalDriver($driver); -} diff --git a/lib/Gedmo/Mapping/Driver/AbstractAnnotationDriver.php b/lib/Gedmo/Mapping/Driver/AbstractAnnotationDriver.php deleted file mode 100644 index 9d3ec636c4..0000000000 --- a/lib/Gedmo/Mapping/Driver/AbstractAnnotationDriver.php +++ /dev/null @@ -1,111 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -abstract class AbstractAnnotationDriver implements AnnotationDriverInterface -{ - /** - * Annotation reader instance - * - * @var object - */ - protected $reader; - - /** - * Original driver if it is available - */ - protected $_originalDriver = null; - - /** - * List of types which are valid for extension - * - * @var array - */ - protected $validTypes = array(); - - /** - * {@inheritDoc} - */ - public function setAnnotationReader($reader) - { - $this->reader = $reader; - } - - /** - * Passes in the mapping read by original driver - * - * @param object $driver - */ - public function setOriginalDriver($driver) - { - $this->_originalDriver = $driver; - } - - /** - * @param object $meta - * - * @return \ReflectionClass - */ - public function getMetaReflectionClass($meta) - { - $class = $meta->getReflectionClass(); - if (!$class) { - // based on recent doctrine 2.3.0-DEV maybe will be fixed in some way - // this happens when running annotation driver in combination with - // static reflection services. This is not the nicest fix - $class = new \ReflectionClass($meta->name); - } - - return $class; - } - - /** - * Checks if $field type is valid - * - * @param object $meta - * @param string $field - * - * @return boolean - */ - protected function isValidField($meta, $field) - { - $mapping = $meta->getFieldMapping($field); - - return $mapping && in_array($mapping['type'], $this->validTypes); - } - - /** - * @param \Doctrine\Common\Persistence\Mapping\ClassMetaData $meta - * @param array $config - */ - public function validateFullMetadata(ClassMetadata $meta, array $config) - { - } - - /** - * Try to find out related class name out of mapping - * - * @param $metadata - the mapped class metadata - * @param $name - the related object class name - * @return string - related class name or empty string if does not exist - */ - protected function getRelatedClassName($metadata, $name) - { - if (class_exists($name) || interface_exists($name)) { - return $name; - } - $refl = $metadata->getReflectionClass(); - $ns = $refl->getNamespaceName(); - $className = $ns . '\\' . $name; - return class_exists($className) ? $className : ''; - } -} diff --git a/lib/Gedmo/Mapping/Driver/AnnotationDriverInterface.php b/lib/Gedmo/Mapping/Driver/AnnotationDriverInterface.php deleted file mode 100644 index 513f3215ab..0000000000 --- a/lib/Gedmo/Mapping/Driver/AnnotationDriverInterface.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface AnnotationDriverInterface extends Driver -{ - /** - * Set annotation reader class - * since older doctrine versions do not provide an interface - * it must provide these methods: - * getClassAnnotations([reflectionClass]) - * getClassAnnotation([reflectionClass], [name]) - * getPropertyAnnotations([reflectionProperty]) - * getPropertyAnnotation([reflectionProperty], [name]) - * - * @param object $reader - annotation reader class - */ - public function setAnnotationReader($reader); -} diff --git a/lib/Gedmo/Mapping/Driver/Chain.php b/lib/Gedmo/Mapping/Driver/Chain.php deleted file mode 100644 index 24b5694bdd..0000000000 --- a/lib/Gedmo/Mapping/Driver/Chain.php +++ /dev/null @@ -1,103 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Chain implements Driver -{ - /** - * The default driver - * - * @var Driver|null - */ - private $defaultDriver; - - /** - * List of drivers nested - * @var Driver[] - */ - private $_drivers = array(); - - /** - * Add a nested driver. - * - * @param Driver $nestedDriver - * @param string $namespace - */ - public function addDriver(Driver $nestedDriver, $namespace) - { - $this->_drivers[$namespace] = $nestedDriver; - } - - /** - * Get the array of nested drivers. - * - * @return Driver[] $drivers - */ - public function getDrivers() - { - return $this->_drivers; - } - - /** - * Get the default driver. - * - * @return Driver|null - */ - public function getDefaultDriver() - { - return $this->defaultDriver; - } - - /** - * Set the default driver. - * - * @param Driver $driver - */ - public function setDefaultDriver(Driver $driver) - { - $this->defaultDriver = $driver; - } - - /** - * {@inheritDoc} - */ - public function readExtendedMetadata($meta, array &$config) - { - foreach ($this->_drivers as $namespace => $driver) { - if (strpos($meta->name, $namespace) === 0) { - $driver->readExtendedMetadata($meta, $config); - - return; - } - } - - if (null !== $this->defaultDriver) { - $this->defaultDriver->readExtendedMetadata($meta, $config); - - return; - } - - // commenting it for customized mapping support, debugging of such cases might get harder - //throw new \Gedmo\Exception\UnexpectedValueException('Class ' . $meta->name . ' is not a valid entity or mapped super class.'); - } - - /** - * Passes in the mapping read by original driver - * - * @param $driver - * @return void - */ - public function setOriginalDriver($driver) - { - //not needed here - } -} diff --git a/lib/Gedmo/Mapping/Event/Adapter/ODM.php b/lib/Gedmo/Mapping/Event/Adapter/ODM.php deleted file mode 100644 index 1a55ddcb83..0000000000 --- a/lib/Gedmo/Mapping/Event/Adapter/ODM.php +++ /dev/null @@ -1,184 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class ODM implements AdapterInterface -{ - /** - * @var \Doctrine\Common\EventArgs - */ - private $args; - - /** - * @var \Doctrine\ODM\MongoDB\DocumentManager - */ - private $dm; - - /** - * {@inheritdoc} - */ - public function setEventArgs(EventArgs $args) - { - $this->args = $args; - } - - /** - * {@inheritdoc} - */ - public function getDomainObjectName() - { - return 'Document'; - } - - /** - * {@inheritdoc} - */ - public function getManagerName() - { - return 'ODM'; - } - - /** - * {@inheritdoc} - */ - public function getRootObjectClass($meta) - { - return $meta->rootDocumentName; - } - - /** - * Set the document manager - * - * @param \Doctrine\ODM\MongoDB\DocumentManager $dm - */ - public function setDocumentManager(DocumentManager $dm) - { - $this->dm = $dm; - } - - /** - * {@inheritdoc} - */ - public function getObjectManager() - { - if (!is_null($this->dm)) { - return $this->dm; - } - - return $this->__call('getDocumentManager', array()); - } - - /** - * {@inheritdoc} - */ - public function getObjectState($uow, $object) - { - return $uow->getDocumentState($object); - } - - /** - * {@inheritdoc} - */ - public function __call($method, $args) - { - if (is_null($this->args)) { - throw new RuntimeException("Event args must be set before calling its methods"); - } - $method = str_replace('Object', $this->getDomainObjectName(), $method); - - return call_user_func_array(array($this->args, $method), $args); - } - - /** - * {@inheritdoc} - */ - public function getObjectChangeSet($uow, $object) - { - return $uow->getDocumentChangeSet($object); - } - - /** - * {@inheritdoc} - */ - public function getSingleIdentifierFieldName($meta) - { - return $meta->identifier; - } - - /** - * {@inheritdoc} - */ - public function recomputeSingleObjectChangeSet($uow, $meta, $object) - { - $uow->recomputeSingleDocumentChangeSet($meta, $object); - } - - /** - * {@inheritdoc} - */ - public function getScheduledObjectUpdates($uow) - { - $updates = $uow->getScheduledDocumentUpdates(); - $upserts = $uow->getScheduledDocumentUpserts(); - - return array_merge($updates, $upserts); - } - - /** - * {@inheritdoc} - */ - public function getScheduledObjectInsertions($uow) - { - return $uow->getScheduledDocumentInsertions(); - } - - /** - * {@inheritdoc} - */ - public function getScheduledObjectDeletions($uow) - { - return $uow->getScheduledDocumentDeletions(); - } - - /** - * {@inheritdoc} - */ - public function setOriginalObjectProperty($uow, $oid, $property, $value) - { - $uow->setOriginalDocumentProperty($oid, $property, $value); - } - - /** - * {@inheritdoc} - */ - public function clearObjectChangeSet($uow, $oid) - { - $uow->clearDocumentChangeSet($oid); - } - - /** - * Creates a ODM specific LifecycleEventArgs. - * - * @param object $document - * @param \Doctrine\ODM\MongoDB\DocumentManager $documentManager - * - * @return \Doctrine\ODM\MongoDB\Event\LifecycleEventArgs - */ - public function createLifecycleEventArgsInstance($document, $documentManager) - { - return new LifecycleEventArgs($document, $documentManager); - } -} diff --git a/lib/Gedmo/Mapping/Event/Adapter/ORM.php b/lib/Gedmo/Mapping/Event/Adapter/ORM.php deleted file mode 100644 index b8582bd4a7..0000000000 --- a/lib/Gedmo/Mapping/Event/Adapter/ORM.php +++ /dev/null @@ -1,181 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class ORM implements AdapterInterface -{ - /** - * @var \Doctrine\Common\EventArgs - */ - private $args; - - /** - * @var \Doctrine\ORM\EntityManager - */ - private $em; - - /** - * {@inheritdoc} - */ - public function setEventArgs(EventArgs $args) - { - $this->args = $args; - } - - /** - * {@inheritdoc} - */ - public function getDomainObjectName() - { - return 'Entity'; - } - - /** - * {@inheritdoc} - */ - public function getManagerName() - { - return 'ORM'; - } - - /** - * {@inheritdoc} - */ - public function getRootObjectClass($meta) - { - return $meta->rootEntityName; - } - - /** - * {@inheritdoc} - */ - public function __call($method, $args) - { - if (is_null($this->args)) { - throw new RuntimeException("Event args must be set before calling its methods"); - } - $method = str_replace('Object', $this->getDomainObjectName(), $method); - - return call_user_func_array(array($this->args, $method), $args); - } - - /** - * Set the entity manager - * - * @param \Doctrine\ORM\EntityManager $em - */ - public function setEntityManager(EntityManager $em) - { - $this->em = $em; - } - - /** - * {@inheritdoc} - */ - public function getObjectManager() - { - if (!is_null($this->em)) { - return $this->em; - } - - return $this->__call('getEntityManager', array()); - } - - /** - * {@inheritdoc} - */ - public function getObjectState($uow, $object) - { - return $uow->getEntityState($object); - } - - /** - * {@inheritdoc} - */ - public function getObjectChangeSet($uow, $object) - { - return $uow->getEntityChangeSet($object); - } - - /** - * {@inheritdoc} - */ - public function getSingleIdentifierFieldName($meta) - { - return $meta->getSingleIdentifierFieldName(); - } - - /** - * {@inheritdoc} - */ - public function recomputeSingleObjectChangeSet($uow, $meta, $object) - { - $uow->recomputeSingleEntityChangeSet($meta, $object); - } - - /** - * {@inheritdoc} - */ - public function getScheduledObjectUpdates($uow) - { - return $uow->getScheduledEntityUpdates(); - } - - /** - * {@inheritdoc} - */ - public function getScheduledObjectInsertions($uow) - { - return $uow->getScheduledEntityInsertions(); - } - - /** - * {@inheritdoc} - */ - public function getScheduledObjectDeletions($uow) - { - return $uow->getScheduledEntityDeletions(); - } - - /** - * {@inheritdoc} - */ - public function setOriginalObjectProperty($uow, $oid, $property, $value) - { - $uow->setOriginalEntityProperty($oid, $property, $value); - } - - /** - * {@inheritdoc} - */ - public function clearObjectChangeSet($uow, $oid) - { - $uow->clearEntityChangeSet($oid); - } - - /** - * Creates a ORM specific LifecycleEventArgs. - * - * @param object $document - * @param \Doctrine\ODM\MongoDB\DocumentManager $documentManager - * - * @return \Doctrine\ODM\MongoDB\Event\LifecycleEventArgs - */ - public function createLifecycleEventArgsInstance($document, $documentManager) - { - return new LifecycleEventArgs($document, $documentManager); - } -} diff --git a/lib/Gedmo/Mapping/Event/AdapterInterface.php b/lib/Gedmo/Mapping/Event/AdapterInterface.php deleted file mode 100644 index 9900d61447..0000000000 --- a/lib/Gedmo/Mapping/Event/AdapterInterface.php +++ /dev/null @@ -1,151 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface AdapterInterface -{ - /** - * Set the eventargs - * - * @param \Doctrine\Common\EventArgs $args - */ - public function setEventArgs(EventArgs $args); - - /** - * Call specific method on event args - * - * @param string $method - * @param array $args - * - * @return mixed - */ - public function __call($method, $args); - - /** - * Get the name of domain object - * - * @return string - */ - public function getDomainObjectName(); - - /** - * Get the name of used manager for this - * event adapter - * - * @return string - */ - public function getManagerName(); - - /** - * Get the root object class, handles inheritance - * - * @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $meta - * - * @return string - */ - public function getRootObjectClass($meta); - - /** - * Get used object manager - * - * @return \Doctrine\Common\Persistence\ObjectManager - */ - public function getObjectManager(); - - /** - * Get object state - * - * @param UnitOfWork $uow - * @param object $object - * - * @return int The document state. - */ - public function getObjectState($uow, $object); - - /** - * Get the object changeset from a UnitOfWork - * - * @param UnitOfWork $uow - * @param object $object - * - * @return array - */ - public function getObjectChangeSet($uow, $object); - - /** - * Get the single identifier field name - * - * @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $meta - * - * @return string - */ - public function getSingleIdentifierFieldName($meta); - - /** - * Recompute the single object changeset from a UnitOfWork - * - * @param UnitOfWork $uow - * @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $meta - * @param object $object - * - * @return void - */ - public function recomputeSingleObjectChangeSet($uow, $meta, $object); - - /** - * Get the scheduled object updates from a UnitOfWork - * - * @param UnitOfWork $uow - * - * @return array - */ - public function getScheduledObjectUpdates($uow); - - /** - * Get the scheduled object insertions from a UnitOfWork - * - * @param UnitOfWork $uow - * - * @return array - */ - public function getScheduledObjectInsertions($uow); - - /** - * Get the scheduled object deletions from a UnitOfWork - * - * @param UnitOfWork $uow - * - * @return array - */ - public function getScheduledObjectDeletions($uow); - - /** - * Sets a property value of the original data array of an object - * - * @param UnitOfWork $uow - * @param string $oid - * @param string $property - * @param mixed $value - * - * @return void - */ - public function setOriginalObjectProperty($uow, $oid, $property, $value); - - /** - * Clears the property changeset of the object with the given OID. - * - * @param UnitOfWork $uow - * @param string $oid The object's OID. - */ - public function clearObjectChangeSet($uow, $oid); -} diff --git a/lib/Gedmo/Mapping/ExtensionMetadataFactory.php b/lib/Gedmo/Mapping/ExtensionMetadataFactory.php deleted file mode 100644 index 1784998743..0000000000 --- a/lib/Gedmo/Mapping/ExtensionMetadataFactory.php +++ /dev/null @@ -1,180 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class ExtensionMetadataFactory -{ - /** - * Extension driver - * @var \Gedmo\Mapping\Driver - */ - protected $driver; - - /** - * Object manager, entity or document - * @var object - */ - protected $objectManager; - - /** - * Extension namespace - * - * @var string - */ - protected $extensionNamespace; - - /** - * Custom annotation reader - * - * @var object - */ - protected $annotationReader; - - /** - * Initializes extension driver - * - * @param ObjectManager $objectManager - * @param string $extensionNamespace - * @param object $annotationReader - */ - public function __construct(ObjectManager $objectManager, $extensionNamespace, $annotationReader) - { - $this->objectManager = $objectManager; - $this->annotationReader = $annotationReader; - $this->extensionNamespace = $extensionNamespace; - $omDriver = $objectManager->getConfiguration()->getMetadataDriverImpl(); - $this->driver = $this->getDriver($omDriver); - } - - /** - * Reads extension metadata - * - * @param object $meta - * @return array - the metatada configuration - */ - public function getExtensionMetadata($meta) - { - if ($meta->isMappedSuperclass) { - return; // ignore mappedSuperclasses for now - } - $config = array(); - $cmf = $this->objectManager->getMetadataFactory(); - $useObjectName = $meta->name; - // collect metadata from inherited classes - if (null !== $meta->reflClass) { - foreach (array_reverse(class_parents($meta->name)) as $parentClass) { - // read only inherited mapped classes - if ($cmf->hasMetadataFor($parentClass)) { - $class = $this->objectManager->getClassMetadata($parentClass); - $this->driver->readExtendedMetadata($class, $config); - $isBaseInheritanceLevel = !$class->isInheritanceTypeNone() - && !$class->parentClasses - && $config - ; - if ($isBaseInheritanceLevel) { - $useObjectName = $class->name; - } - } - } - $this->driver->readExtendedMetadata($meta, $config); - } - if ($config) { - $config['useObjectClass'] = $useObjectName; - } - - // cache the metadata (even if it's empty) - // caching empty metadata will prevent re-parsing non-existent annotations - $cacheId = self::getCacheId($meta->name, $this->extensionNamespace); - if ($cacheDriver = $cmf->getCacheDriver()) { - $cacheDriver->save($cacheId, $config, null); - } - - return $config; - } - - /** - * Get the cache id - * - * @param string $className - * @param string $extensionNamespace - * @return string - */ - public static function getCacheId($className, $extensionNamespace) - { - return $className.'\\$'.strtoupper(str_replace('\\', '_', $extensionNamespace)).'_CLASSMETADATA'; - } - - /** - * Get the extended driver instance which will - * read the metadata required by extension - * - * @param object $omDriver - * @throws \Gedmo\Exception\RuntimeException if driver was not found in extension - * @return \Gedmo\Mapping\Driver - */ - protected function getDriver($omDriver) - { - $driver = null; - $className = get_class($omDriver); - $driverName = substr($className, strrpos($className, '\\') + 1); - if ($omDriver instanceof MappingDriverChain || $driverName == 'DriverChain') { - $driver = new Driver\Chain(); - foreach ($omDriver->getDrivers() as $namespace => $nestedOmDriver) { - $driver->addDriver($this->getDriver($nestedOmDriver), $namespace); - } - if (version_compare(CommonLibVer::VERSION, '2.3.0', '>=') && $omDriver->getDefaultDriver() !== null) { - $driver->setDefaultDriver($this->getDriver($omDriver->getDefaultDriver())); - } - } else { - $driverName = substr($driverName, 0, strpos($driverName, 'Driver')); - $isSimplified = false; - if (substr($driverName, 0, 10) === 'Simplified') { - // support for simplified file drivers - $driverName = substr($driverName, 10); - $isSimplified = true; - } - // create driver instance - $driverClassName = $this->extensionNamespace.'\Mapping\Driver\\'.$driverName; - if (!class_exists($driverClassName)) { - $driverClassName = $this->extensionNamespace.'\Mapping\Driver\Annotation'; - if (!class_exists($driverClassName)) { - throw new \Gedmo\Exception\RuntimeException("Failed to fallback to annotation driver: ({$driverClassName}), extension driver was not found."); - } - } - $driver = new $driverClassName(); - $driver->setOriginalDriver($omDriver); - if ($driver instanceof FileDriver) { - /** @var $driver FileDriver */ - if ($omDriver instanceof MappingDriver) { - $driver->setLocator($omDriver->getLocator()); - // BC for Doctrine 2.2 - } elseif ($isSimplified) { - $driver->setLocator(new SymfonyFileLocator($omDriver->getNamespacePrefixes(), $omDriver->getFileExtension())); - } else { - $driver->setLocator(new DefaultFileLocator($omDriver->getPaths(), $omDriver->getFileExtension())); - } - } - if ($driver instanceof AnnotationDriverInterface) { - $driver->setAnnotationReader($this->annotationReader); - } - } - - return $driver; - } -} diff --git a/lib/Gedmo/Mapping/MappedEventSubscriber.php b/lib/Gedmo/Mapping/MappedEventSubscriber.php deleted file mode 100644 index 0c495ea860..0000000000 --- a/lib/Gedmo/Mapping/MappedEventSubscriber.php +++ /dev/null @@ -1,262 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -abstract class MappedEventSubscriber implements EventSubscriber -{ - /** - * Static List of cached object configurations - * leaving it static for reasons to look into - * other listener configuration - * - * @var array - */ - protected static $configurations = array(); - - /** - * Listener name, etc: sluggable - * - * @var string - */ - protected $name; - - /** - * ExtensionMetadataFactory used to read the extension - * metadata through the extension drivers - * - * @var ExtensionMetadataFactory - */ - private $extensionMetadataFactory = array(); - - /** - * List of event adapters used for this listener - * - * @var array - */ - private $adapters = array(); - - /** - * Custom annotation reader - * - * @var object - */ - private $annotationReader; - - /** - * @var \Doctrine\Common\Annotations\AnnotationReader - */ - private static $defaultAnnotationReader; - - /** - * Constructor - */ - public function __construct() - { - $parts = explode('\\', $this->getNamespace()); - $this->name = end($parts); - } - - /** - * Get an event adapter to handle event specific - * methods - * - * @param EventArgs $args - * - * @throws \Gedmo\Exception\InvalidArgumentException - if event is not recognized - * - * @return \Gedmo\Mapping\Event\AdapterInterface - */ - protected function getEventAdapter(EventArgs $args) - { - $class = get_class($args); - if (preg_match('@Doctrine\\\([^\\\]+)@', $class, $m) && in_array($m[1], array('ODM', 'ORM'))) { - if (!isset($this->adapters[$m[1]])) { - $adapterClass = $this->getNamespace().'\\Mapping\\Event\\Adapter\\'.$m[1]; - if (!class_exists($adapterClass)) { - $adapterClass = 'Gedmo\\Mapping\\Event\\Adapter\\'.$m[1]; - } - $this->adapters[$m[1]] = new $adapterClass(); - } - $this->adapters[$m[1]]->setEventArgs($args); - - return $this->adapters[$m[1]]; - } else { - throw new \Gedmo\Exception\InvalidArgumentException('Event mapper does not support event arg class: '.$class); - } - } - - /** - * Get the configuration for specific object class - * if cache driver is present it scans it also - * - * @param ObjectManager $objectManager - * @param string $class - * - * @return array - */ - public function getConfiguration(ObjectManager $objectManager, $class) - { - $config = array(); - if (isset(self::$configurations[$this->name][$class])) { - $config = self::$configurations[$this->name][$class]; - } else { - $factory = $objectManager->getMetadataFactory(); - $cacheDriver = $factory->getCacheDriver(); - if ($cacheDriver) { - $cacheId = ExtensionMetadataFactory::getCacheId($class, $this->getNamespace()); - if (($cached = $cacheDriver->fetch($cacheId)) !== false) { - self::$configurations[$this->name][$class] = $cached; - $config = $cached; - } else { - // re-generate metadata on cache miss - $this->loadMetadataForObjectClass($objectManager, $factory->getMetadataFor($class)); - if (isset(self::$configurations[$this->name][$class])) { - $config = self::$configurations[$this->name][$class]; - } - } - - $objectClass = isset($config['useObjectClass']) ? $config['useObjectClass'] : $class; - if ($objectClass !== $class) { - $this->getConfiguration($objectManager, $objectClass); - } - } - } - - return $config; - } - - /** - * Get extended metadata mapping reader - * - * @param ObjectManager $objectManager - * - * @return ExtensionMetadataFactory - */ - public function getExtensionMetadataFactory(ObjectManager $objectManager) - { - $oid = spl_object_hash($objectManager); - if (!isset($this->extensionMetadataFactory[$oid])) { - if (is_null($this->annotationReader)) { - // create default annotation reader for extensions - $this->annotationReader = $this->getDefaultAnnotationReader(); - } - $this->extensionMetadataFactory[$oid] = new ExtensionMetadataFactory( - $objectManager, - $this->getNamespace(), - $this->annotationReader - ); - } - - return $this->extensionMetadataFactory[$oid]; - } - - /** - * Set annotation reader class - * since older doctrine versions do not provide an interface - * it must provide these methods: - * getClassAnnotations([reflectionClass]) - * getClassAnnotation([reflectionClass], [name]) - * getPropertyAnnotations([reflectionProperty]) - * getPropertyAnnotation([reflectionProperty], [name]) - * - * @param Reader $reader - annotation reader class - */ - public function setAnnotationReader($reader) - { - $this->annotationReader = $reader; - } - - /** - * Scans the objects for extended annotations - * event subscribers must subscribe to loadClassMetadata event - * - * @param ObjectManager $objectManager - * @param object $metadata - * @return void - */ - public function loadMetadataForObjectClass(ObjectManager $objectManager, $metadata) - { - $factory = $this->getExtensionMetadataFactory($objectManager); - try { - $config = $factory->getExtensionMetadata($metadata); - } catch (\ReflectionException $e) { - // entity\document generator is running - $config = false; // will not store a cached version, to remap later - } - if ($config) { - self::$configurations[$this->name][$metadata->name] = $config; - } - } - - /** - * Get the namespace of extension event subscriber. - * used for cache id of extensions also to know where - * to find Mapping drivers and event adapters - * - * @return string - */ - abstract protected function getNamespace(); - - /** - * Create default annotation reader for extensions - * - * @return \Doctrine\Common\Annotations\AnnotationReader - */ - private function getDefaultAnnotationReader() - { - if (null === self::$defaultAnnotationReader) { - if (version_compare(\Doctrine\Common\Version::VERSION, '2.2.0-DEV', '>=')) { - $reader = new \Doctrine\Common\Annotations\AnnotationReader(); - \Doctrine\Common\Annotations\AnnotationRegistry::registerAutoloadNamespace( - 'Gedmo\\Mapping\\Annotation', - __DIR__.'/../../' - ); - $reader = new \Doctrine\Common\Annotations\CachedReader($reader, new ArrayCache()); - } elseif (version_compare(\Doctrine\Common\Version::VERSION, '2.1.0RC4-DEV', '>=')) { - $reader = new \Doctrine\Common\Annotations\AnnotationReader(); - \Doctrine\Common\Annotations\AnnotationRegistry::registerAutoloadNamespace( - 'Gedmo\\Mapping\\Annotation', - __DIR__.'/../../' - ); - $reader->setDefaultAnnotationNamespace('Doctrine\ORM\Mapping\\'); - $reader = new \Doctrine\Common\Annotations\CachedReader($reader, new ArrayCache()); - } elseif (version_compare(\Doctrine\Common\Version::VERSION, '2.1.0-BETA3-DEV', '>=')) { - $reader = new \Doctrine\Common\Annotations\AnnotationReader(); - $reader->setDefaultAnnotationNamespace('Doctrine\ORM\Mapping\\'); - $reader->setIgnoreNotImportedAnnotations(true); - $reader->setAnnotationNamespaceAlias('Gedmo\\Mapping\\Annotation\\', 'gedmo'); - $reader->setEnableParsePhpImports(false); - $reader->setAutoloadAnnotations(true); - $reader = new \Doctrine\Common\Annotations\CachedReader( - new \Doctrine\Common\Annotations\IndexedReader($reader), new ArrayCache() - ); - } else { - $reader = new \Doctrine\Common\Annotations\AnnotationReader(); - $reader->setAutoloadAnnotations(true); - $reader->setAnnotationNamespaceAlias('Gedmo\\Mapping\\Annotation\\', 'gedmo'); - $reader->setDefaultAnnotationNamespace('Doctrine\ORM\Mapping\\'); - } - self::$defaultAnnotationReader = $reader; - } - - return self::$defaultAnnotationReader; - } -} diff --git a/lib/Gedmo/ReferenceIntegrity/Mapping/Driver/Annotation.php b/lib/Gedmo/ReferenceIntegrity/Mapping/Driver/Annotation.php deleted file mode 100644 index 72730bdbac..0000000000 --- a/lib/Gedmo/ReferenceIntegrity/Mapping/Driver/Annotation.php +++ /dev/null @@ -1,77 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Annotation extends AbstractAnnotationDriver -{ - /** - * Annotation to identify the fields which manages the reference integrity - */ - const REFERENCE_INTEGRITY = 'Gedmo\\Mapping\\Annotation\\ReferenceIntegrity'; - - /** - * ReferenceIntegrityAction extension annotation - */ - const ACTION = 'Gedmo\\Mapping\\Annotation\\ReferenceIntegrityAction'; - - /** - * {@inheritDoc} - */ - public function readExtendedMetadata($meta, array &$config) - { - $validator = new Validator(); - $reflClass = $this->getMetaReflectionClass($meta); - - foreach ($reflClass->getProperties() as $reflProperty) { - if ($referenceIntegrity = $this->reader->getPropertyAnnotation($reflProperty, self::REFERENCE_INTEGRITY)) { - $property = $reflProperty->getName(); - if (!$meta->hasField($property)) { - throw new InvalidMappingException( - sprintf( - "Unable to find reference integrity [%s] as mapped property in entity - %s", - $property, - $meta->name - ) - ); - } - - $fieldMapping = $meta->getFieldMapping($property); - if (!isset($fieldMapping['mappedBy'])) { - throw new InvalidMappingException( - sprintf( - "'mappedBy' should be set on '%s' in '%s'", - $property, - $meta->name - ) - ); - } - - if (!in_array($referenceIntegrity->value, $validator->getIntegrityActions())) { - throw new InvalidMappingException( - sprintf( - "Field - [%s] does not have a valid integrity option, [%s] in class - %s", - $property, - implode($validator->getIntegrityActions(), ', '), - $meta->name - ) - ); - } - - $config['referenceIntegrity'][$property] = $referenceIntegrity->value; - } - } - } -} diff --git a/lib/Gedmo/ReferenceIntegrity/Mapping/Validator.php b/lib/Gedmo/ReferenceIntegrity/Mapping/Validator.php deleted file mode 100644 index d1f0d5147f..0000000000 --- a/lib/Gedmo/ReferenceIntegrity/Mapping/Validator.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ - -class Validator -{ - const NULLIFY = 'nullify'; - const PULL = 'pull'; - const RESTRICT = 'restrict'; - - /** - * List of actions which are valid as integrity check - * - * @var array - */ - private $integrityActions = array( - self::NULLIFY, - self::PULL, - self::RESTRICT, - ); - - /** - * Returns a list of available integrity actions - * - * @return array - */ - public function getIntegrityActions() - { - return $this->integrityActions; - } -} diff --git a/lib/Gedmo/ReferenceIntegrity/ReferenceIntegrityListener.php b/lib/Gedmo/ReferenceIntegrity/ReferenceIntegrityListener.php deleted file mode 100644 index 760a16da14..0000000000 --- a/lib/Gedmo/ReferenceIntegrity/ReferenceIntegrityListener.php +++ /dev/null @@ -1,181 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class ReferenceIntegrityListener extends MappedEventSubscriber -{ - /** - * {@inheritDoc} - */ - public function getSubscribedEvents() - { - return array( - 'loadClassMetadata', - 'preRemove', - ); - } - - /** - * Maps additional metadata for the Document - * - * @param EventArgs $eventArgs - * @return void - */ - public function loadClassMetadata(EventArgs $eventArgs) - { - $ea = $this->getEventAdapter($eventArgs); - $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata()); - } - - /** - * Looks for referenced objects being removed - * to nullify the relation or throw an exception - * - * @param EventArgs $args - * @return void - */ - public function preRemove(EventArgs $args) - { - $ea = $this->getEventAdapter($args); - $om = $ea->getObjectManager(); - $object = $ea->getObject(); - $class = get_class($object); - $meta = $om->getClassMetadata($class); - - if ($config = $this->getConfiguration($om, $class)) { - foreach ($config['referenceIntegrity'] as $property => $action) { - $reflProp = $meta->getReflectionProperty($property); - $refDoc = $reflProp->getValue($object); - $fieldMapping = $meta->getFieldMapping($property); - - switch ($action) { - case Validator::NULLIFY: - if (!isset($fieldMapping['mappedBy'])) { - throw new InvalidMappingException( - sprintf( - "Reference '%s' on '%s' should have 'mappedBy' option defined", - $property, - $meta->name - ) - ); - } - - $subMeta = $om->getClassMetadata($fieldMapping['targetDocument']); - - if (!$subMeta->hasField($fieldMapping['mappedBy'])) { - throw new InvalidMappingException( - sprintf( - "Unable to find reference integrity [%s] as mapped property in entity - %s", - $fieldMapping['mappedBy'], - $fieldMapping['targetDocument'] - ) - ); - } - - $refReflProp = $subMeta->getReflectionProperty($fieldMapping['mappedBy']); - - if ($meta->isCollectionValuedReference($property)) { - foreach ($refDoc as $refObj) { - $refReflProp->setValue($refObj, null); - $om->persist($refObj); - } - } else { - $refReflProp->setValue($refDoc, null); - $om->persist($refDoc); - } - - break; - case Validator::PULL: - if (!isset($fieldMapping['mappedBy'])) { - throw new InvalidMappingException( - sprintf( - "Reference '%s' on '%s' should have 'mappedBy' option defined", - $property, - $meta->name - ) - ); - } - - $subMeta = $om->getClassMetadata($fieldMapping['targetDocument']); - - if (!$subMeta->hasField($fieldMapping['mappedBy'])) { - throw new InvalidMappingException( - sprintf( - "Unable to find reference integrity [%s] as mapped property in entity - %s", - $fieldMapping['mappedBy'], - $fieldMapping['targetDocument'] - ) - ); - } - - if (!$subMeta->isCollectionValuedReference($fieldMapping['mappedBy'])) { - throw new InvalidMappingException( - sprintf( - "Reference integrity [%s] mapped property in entity - %s should be a Reference Many", - $fieldMapping['mappedBy'], - $fieldMapping['targetDocument'] - ) - ); - } - - $refReflProp = $subMeta->getReflectionProperty($fieldMapping['mappedBy']); - - if ($meta->isCollectionValuedReference($property)) { - foreach ($refDoc as $refObj) { - $collection = $refReflProp->getValue($refObj); - $collection->removeElement($object); - $refReflProp->setValue($refObj, $collection); - $om->persist($refObj); - } - } else if (is_object($refDoc)) { - $collection = $refReflProp->getValue($refDoc); - $collection->removeElement($object); - $refReflProp->setValue($refDoc, $collection); - $om->persist($refDoc); - } - - break; - case Validator::RESTRICT: - if ($meta->isCollectionValuedReference($property) && $refDoc->count() > 0) { - throw new ReferenceIntegrityStrictException( - sprintf( - "The reference integrity for the '%s' collection is restricted", - $fieldMapping['targetDocument'] - ) - ); - } - if ($meta->isSingleValuedReference($property) && !is_null($refDoc)) { - throw new ReferenceIntegrityStrictException( - sprintf( - "The reference integrity for the '%s' document is restricted", - $fieldMapping['targetDocument'] - ) - ); - } - - break; - } - } - } - } - - /** - * {@inheritDoc} - */ - protected function getNamespace() - { - return __NAMESPACE__; - } -} diff --git a/lib/Gedmo/References/LazyCollection.php b/lib/Gedmo/References/LazyCollection.php deleted file mode 100644 index f28bf47d72..0000000000 --- a/lib/Gedmo/References/LazyCollection.php +++ /dev/null @@ -1,241 +0,0 @@ - - * @author Bulat Shakirzyanov - * @author Jonathan H. Wage - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class LazyCollection implements Collection -{ - private $results; - private $callback; - - public function __construct($callback) - { - $this->callback = $callback; - } - - public function add($element) - { - $this->initialize(); - - return $this->results->add($element); - } - - public function clear() - { - $this->initialize(); - - return $this->results->clear(); - } - - public function contains($element) - { - $this->initialize(); - - return $this->results->contains($element); - } - - public function containsKey($key) - { - $this->initialize(); - - return $this->results->containsKey($key); - } - - public function current() - { - $this->initialize(); - - return $this->results->current(); - } - - public function exists(\Closure $p) - { - $this->initialize(); - - return $this->results->exists($p); - } - - public function filter(\Closure $p) - { - $this->initialize(); - - return $this->results->filter($p); - } - - public function first() - { - $this->initialize(); - - return $this->results->first(); - } - - public function forAll(\Closure $p) - { - $this->initialize(); - - return $this->results->forAll($p); - } - - public function get($key) - { - $this->initialize(); - - return $this->results->get($key); - } - - public function getKeys() - { - $this->initialize(); - - return $this->results->getKeys(); - } - - public function getValues() - { - $this->initialize(); - - return $this->results->getValues(); - } - - public function indexOf($element) - { - $this->initialize(); - - return $this->results->indexOf($element); - } - - public function isEmpty() - { - $this->initialize(); - - return $this->results->isEmpty(); - } - - public function key() - { - $this->initialize(); - - return $this->results->key(); - } - - public function last() - { - $this->initialize(); - - return $this->results->last(); - } - - public function map(\Closure $func) - { - $this->initialize(); - - return $this->results->map($func); - } - - public function next() - { - $this->initialize(); - - return $this->results->next(); - } - - public function partition(\Closure $p) - { - $this->initialize(); - - return $this->results->partition($p); - } - - public function remove($key) - { - $this->initialize(); - - return $this->results->remove($key); - } - - public function removeElement($element) - { - $this->initialize(); - - return $this->results->removeElement($element); - } - - public function set($key, $value) - { - $this->initialize(); - - return $this->results->set($key, $value); - } - - public function slice($offset, $length = null) - { - $this->initialize(); - - return $this->results->slice($offset, $length); - } - - public function toArray() - { - $this->initialize(); - - return $this->results->toArray(); - } - - public function offsetExists($offset) - { - $this->initialize(); - - return $this->results->offsetExists($offset); - } - - public function offsetGet($offset) - { - $this->initialize(); - - return $this->results->offsetGet($offset); - } - - public function offsetSet($offset, $value) - { - $this->initialize(); - - return $this->results->offsetSet($offset, $value); - } - - public function offsetUnset($offset) - { - $this->initialize(); - - return $this->results->offsetUnset($offset); - } - - public function getIterator() - { - $this->initialize(); - - return $this->results->getIterator(); - } - - public function count() - { - $this->initialize(); - - return $this->results->count(); - } - - private function initialize() - { - if (null === $this->results) { - $this->results = call_user_func($this->callback); - } - } -} diff --git a/lib/Gedmo/References/Mapping/Driver/Annotation.php b/lib/Gedmo/References/Mapping/Driver/Annotation.php deleted file mode 100644 index 2b1f278a4a..0000000000 --- a/lib/Gedmo/References/Mapping/Driver/Annotation.php +++ /dev/null @@ -1,99 +0,0 @@ - - * @author Bulat Shakirzyanov - * @author Jonathan H. Wage - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Annotation implements AnnotationDriverInterface -{ - /** - * Annotation to mark field as reference to one - */ - const REFERENCE_ONE = 'Gedmo\\Mapping\\Annotation\\ReferenceOne'; - - /** - * Annotation to mark field as reference to many - */ - const REFERENCE_MANY = 'Gedmo\\Mapping\\Annotation\\ReferenceMany'; - - /** - * Annotation to mark field as reference to many - */ - const REFERENCE_MANY_EMBED = 'Gedmo\\Mapping\\Annotation\\ReferenceManyEmbed'; - - private $annotations = array( - 'referenceOne' => self::REFERENCE_ONE, - 'referenceMany' => self::REFERENCE_MANY, - 'referenceManyEmbed' => self::REFERENCE_MANY_EMBED, - ); - - /** - * Annotation reader instance - * - * @var object - */ - private $reader; - - /** - * original driver if it is available - */ - protected $_originalDriver = null; - - /** - * {@inheritDoc} - */ - public function setAnnotationReader($reader) - { - $this->reader = $reader; - } - - /** - * {@inheritDoc} - */ - public function readExtendedMetadata($meta, array &$config) - { - $class = $meta->getReflectionClass(); - foreach ($this->annotations as $key => $annotation) { - $config[$key] = array(); - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() || - $meta->isInheritedField($property->name) || - isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - - if ($reference = $this->reader->getPropertyAnnotation($property, $annotation)) { - $config[$key][$property->getName()] = array( - 'field' => $property->getName(), - 'type' => $reference->type, - 'class' => $reference->class, - 'identifier' => $reference->identifier, - 'mappedBy' => $reference->mappedBy, - 'inversedBy' => $reference->inversedBy, - ); - } - } - } - } - - /** - * Passes in the mapping read by original driver - * - * @param $driver - * @return void - */ - public function setOriginalDriver($driver) - { - $this->_originalDriver = $driver; - } -} diff --git a/lib/Gedmo/References/Mapping/Event/ReferencesAdapter.php b/lib/Gedmo/References/Mapping/Event/ReferencesAdapter.php deleted file mode 100644 index 6ec827d30e..0000000000 --- a/lib/Gedmo/References/Mapping/Event/ReferencesAdapter.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @author Bulat Shakirzyanov - * @author Jonathan H. Wage - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface ReferencesAdapter extends AdapterInterface -{ - /** - * Gets the identifier of the given object using the passed ObjectManager. - * - * @param ObjectManager $om - * @param object $object - * @param bool $single - * - * @return array|string|int $id - array or single identifier - */ - public function getIdentifier($om, $object, $single = true); - - /** - * Gets a single reference for the given ObjectManager, class and identifier. - * - * @param ObjectManager $om - * @param string $class - * @param array|string|int $identifier - **/ - public function getSingleReference($om, $class, $identifier); - - /** - * Extracts identifiers from object or proxy. - * - * @param ObjectManager $om - * @param object $object - * @param bool $single - * - * @return array|string|int - array or single identifier - */ - public function extractIdentifier($om, $object, $single = true); -} diff --git a/lib/Gedmo/Sluggable/Handler/SlugHandlerInterface.php b/lib/Gedmo/Sluggable/Handler/SlugHandlerInterface.php deleted file mode 100644 index 8b90c868c9..0000000000 --- a/lib/Gedmo/Sluggable/Handler/SlugHandlerInterface.php +++ /dev/null @@ -1,78 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface SlugHandlerInterface -{ - /** - * Construct the slug handler - * - * @param SluggableListener $sluggable - */ - public function __construct(SluggableListener $sluggable); - - /** - * Callback on slug handlers before the decision - * is made whether or not the slug needs to be - * recalculated - * - * @param SluggableAdapter $ea - * @param array $config - * @param object $object - * @param string $slug - * @param boolean $needToChangeSlug - * - * @return void - */ - public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug); - - /** - * Callback on slug handlers right after the slug is built - * - * @param SluggableAdapter $ea - * @param array $config - * @param object $object - * @param string $slug - * - * @return void - */ - public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug); - - /** - * Callback for slug handlers on slug completion - * - * @param SluggableAdapter $ea - * @param array $config - * @param object $object - * @param string $slug - * - * @return void - */ - public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug); - - /** - * @return boolean whether or not this handler has already urlized the slug - */ - public function handlesUrlization(); - - /** - * Validate handler options - * - * @param array $options - * @param ClassMetadata $meta - */ - public static function validate(array $options, ClassMetadata $meta); -} diff --git a/lib/Gedmo/Sluggable/Mapping/Driver/Annotation.php b/lib/Gedmo/Sluggable/Mapping/Driver/Annotation.php deleted file mode 100644 index 820efeb054..0000000000 --- a/lib/Gedmo/Sluggable/Mapping/Driver/Annotation.php +++ /dev/null @@ -1,142 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Annotation extends AbstractAnnotationDriver -{ - /** - * Annotation to identify field as one which holds the slug - * together with slug options - */ - const SLUG = 'Gedmo\\Mapping\\Annotation\\Slug'; - - /** - * SlugHandler extension annotation - */ - const HANDLER = 'Gedmo\\Mapping\\Annotation\\SlugHandler'; - - /** - * SlugHandler option annotation - */ - const HANDLER_OPTION = 'Gedmo\\Mapping\\Annotation\\SlugHandlerOption'; - - /** - * List of types which are valid for slug and sluggable fields - * - * @var array - */ - protected $validTypes = array( - 'string', - 'text', - 'integer', - 'int', - 'datetime', - 'citext', - ); - - /** - * {@inheritDoc} - */ - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - // property annotations - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() || - $meta->isInheritedField($property->name) || - isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - // slug property - if ($slug = $this->reader->getPropertyAnnotation($property, self::SLUG)) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find slug [{$field}] as mapped property in entity - {$meta->name}"); - } - if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Cannot use field - [{$field}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->name}"); - } - // process slug handlers - $handlers = array(); - if (is_array($slug->handlers) && $slug->handlers) { - foreach ($slug->handlers as $handler) { - if (!$handler instanceof SlugHandler) { - throw new InvalidMappingException("SlugHandler: {$handler} should be instance of SlugHandler annotation in entity - {$meta->name}"); - } - if (!strlen($handler->class)) { - throw new InvalidMappingException("SlugHandler class: {$handler->class} should be a valid class name in entity - {$meta->name}"); - } - $class = $handler->class; - $handlers[$class] = array(); - foreach ((array) $handler->options as $option) { - if (!$option instanceof SlugHandlerOption) { - throw new InvalidMappingException("SlugHandlerOption: {$option} should be instance of SlugHandlerOption annotation in entity - {$meta->name}"); - } - if (!strlen($option->name)) { - throw new InvalidMappingException("SlugHandlerOption name: {$option->name} should be valid name in entity - {$meta->name}"); - } - $handlers[$class][$option->name] = $option->value; - } - $class::validate($handlers[$class], $meta); - } - } - // process slug fields - if (empty($slug->fields) || !is_array($slug->fields)) { - throw new InvalidMappingException("Slug must contain at least one field for slug generation in class - {$meta->name}"); - } - foreach ($slug->fields as $slugField) { - if (!$meta->hasField($slugField)) { - throw new InvalidMappingException("Unable to find slug [{$slugField}] as mapped property in entity - {$meta->name}"); - } - if (!$this->isValidField($meta, $slugField)) { - throw new InvalidMappingException("Cannot use field - [{$slugField}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->name}"); - } - } - if (!is_bool($slug->updatable)) { - throw new InvalidMappingException("Slug annotation [updatable], type is not valid and must be 'boolean' in class - {$meta->name}"); - } - if (!is_bool($slug->unique)) { - throw new InvalidMappingException("Slug annotation [unique], type is not valid and must be 'boolean' in class - {$meta->name}"); - } - if (!empty($meta->identifier) && $meta->isIdentifier($field) && !(bool) $slug->unique) { - throw new InvalidMappingException("Identifier field - [{$field}] slug must be unique in order to maintain primary key in class - {$meta->name}"); - } - if ($slug->unique === false && $slug->unique_base) { - throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'"); - } - if ($slug->unique_base && !$meta->hasField($slug->unique_base) && !$meta->hasAssociation($slug->unique_base)) { - throw new InvalidMappingException("Unable to find [{$slug->unique_base}] as mapped property in entity - {$meta->name}"); - } - // set all options - $config['slugs'][$field] = array( - 'fields' => $slug->fields, - 'slug' => $field, - 'style' => $slug->style, - 'dateFormat' => $slug->dateFormat, - 'updatable' => $slug->updatable, - 'unique' => $slug->unique, - 'unique_base' => $slug->unique_base, - 'separator' => $slug->separator, - 'prefix' => $slug->prefix, - 'suffix' => $slug->suffix, - 'handlers' => $handlers, - ); - } - } - } -} diff --git a/lib/Gedmo/Sluggable/Mapping/Driver/Xml.php b/lib/Gedmo/Sluggable/Mapping/Driver/Xml.php deleted file mode 100644 index baa5e77767..0000000000 --- a/lib/Gedmo/Sluggable/Mapping/Driver/Xml.php +++ /dev/null @@ -1,136 +0,0 @@ - - * @author Miha Vrhovnik - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Xml extends BaseXml -{ - /** - * List of types which are valid for slug and sluggable fields - * - * @var array - */ - private $validTypes = array( - 'string', - 'text', - 'integer', - 'int', - 'datetime', - 'citext', - ); - - /** - * {@inheritDoc} - */ - public function readExtendedMetadata($meta, array &$config) - { - /** - * @var \SimpleXmlElement $xml - */ - $xml = $this->_getMapping($meta->name); - - if (isset($xml->field)) { - foreach ($xml->field as $mapping) { - $mappingDoctrine = $mapping; - /** - * @var \SimpleXmlElement $mapping - */ - $mapping = $mapping->children(self::GEDMO_NAMESPACE_URI); - - $field = $this->_getAttribute($mappingDoctrine, 'name'); - if (isset($mapping->slug)) { - /** - * @var \SimpleXmlElement $slug - */ - $slug = $mapping->slug; - if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Cannot use field - [{$field}] for slug storage, type is not valid and must be 'string' in class - {$meta->name}"); - } - $fields = array_map('trim', explode(',', (string) $this->_getAttribute($slug, 'fields'))); - foreach ($fields as $slugField) { - if (!$meta->hasField($slugField)) { - throw new InvalidMappingException("Unable to find slug [{$slugField}] as mapped property in entity - {$meta->name}"); - } - if (!$this->isValidField($meta, $slugField)) { - throw new InvalidMappingException("Cannot use field - [{$slugField}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->name}"); - } - } - - $handlers = array(); - if (isset($slug->handler)) { - foreach ($slug->handler as $handler) { - $class = (string) $this->_getAttribute($handler, 'class'); - $handlers[$class] = array(); - foreach ($handler->{'handler-option'} as $option) { - $handlers[$class][(string) $this->_getAttribute($option, 'name')] - = (string) $this->_getAttribute($option, 'value') - ; - } - $class::validate($handlers[$class], $meta); - } - } - - // set all options - $config['slugs'][$field] = array( - 'fields' => $fields, - 'slug' => $field, - 'style' => $this->_isAttributeSet($slug, 'style') ? - $this->_getAttribute($slug, 'style') : 'default', - 'updatable' => $this->_isAttributeSet($slug, 'updatable') ? - $this->_getBooleanAttribute($slug, 'updatable') : true, - 'dateFormat' => $this->_isAttributeSet($slug, 'dateFormat') ? - $this->_getAttribute($slug, 'dateFormat') : 'Y-m-d-H:i', - 'unique' => $this->_isAttributeSet($slug, 'unique') ? - $this->_getBooleanAttribute($slug, 'unique') : true, - 'unique_base' => $this->_isAttributeSet($slug, 'unique-base') ? - $this->_getAttribute($slug, 'unique-base') : null, - 'separator' => $this->_isAttributeSet($slug, 'separator') ? - $this->_getAttribute($slug, 'separator') : '-', - 'prefix' => $this->_isAttributeSet($slug, 'prefix') ? - $this->_getAttribute($slug, 'prefix') : '', - 'suffix' => $this->_isAttributeSet($slug, 'suffix') ? - $this->_getAttribute($slug, 'suffix') : '', - 'handlers' => $handlers, - ); - if (!$meta->isMappedSuperclass && $meta->isIdentifier($field) && !$config['slugs'][$field]['unique']) { - throw new InvalidMappingException("Identifier field - [{$field}] slug must be unique in order to maintain primary key in class - {$meta->name}"); - } - $ubase = $config['slugs'][$field]['unique_base']; - if ($config['slugs'][$field]['unique'] === false && $ubase) { - throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'"); - } - if ($ubase && !$meta->hasField($ubase) && !$meta->hasAssociation($ubase)) { - throw new InvalidMappingException("Unable to find [{$ubase}] as mapped property in entity - {$meta->name}"); - } - } - } - } - } - - /** - * Checks if $field type is valid as Sluggable field - * - * @param object $meta - * @param string $field - * - * @return boolean - */ - protected function isValidField($meta, $field) - { - $mapping = $meta->getFieldMapping($field); - - return $mapping && in_array($mapping['type'], $this->validTypes); - } -} diff --git a/lib/Gedmo/Sluggable/Mapping/Driver/Yaml.php b/lib/Gedmo/Sluggable/Mapping/Driver/Yaml.php deleted file mode 100644 index bb3e6599a0..0000000000 --- a/lib/Gedmo/Sluggable/Mapping/Driver/Yaml.php +++ /dev/null @@ -1,144 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Yaml extends File implements Driver -{ - /** - * File extension - * @var string - */ - protected $_extension = '.dcm.yml'; - - /** - * List of types which are valid for slug and sluggable fields - * - * @var array - */ - private $validTypes = array( - 'string', - 'text', - 'integer', - 'int', - 'datetime', - 'citext', - ); - - /** - * {@inheritDoc} - */ - public function readExtendedMetadata($meta, array &$config) - { - $mapping = $this->_getMapping($meta->name); - - if (isset($mapping['fields'])) { - foreach ($mapping['fields'] as $field => $fieldMapping) { - if (isset($fieldMapping['gedmo'])) { - if (isset($fieldMapping['gedmo']['slug'])) { - $slug = $fieldMapping['gedmo']['slug']; - if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Cannot use field - [{$field}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->name}"); - } - // process slug handlers - $handlers = array(); - if (isset($slug['handlers'])) { - foreach ($slug['handlers'] as $handlerClass => $options) { - if (!strlen($handlerClass)) { - throw new InvalidMappingException("SlugHandler class: {$handlerClass} should be a valid class name in entity - {$meta->name}"); - } - $handlers[$handlerClass] = $options; - $handlerClass::validate($handlers[$handlerClass], $meta); - } - } - // process slug fields - if (empty($slug['fields']) || !is_array($slug['fields'])) { - throw new InvalidMappingException("Slug must contain at least one field for slug generation in class - {$meta->name}"); - } - foreach ($slug['fields'] as $slugField) { - if (!$meta->hasField($slugField)) { - throw new InvalidMappingException("Unable to find slug [{$slugField}] as mapped property in entity - {$meta->name}"); - } - if (!$this->isValidField($meta, $slugField)) { - throw new InvalidMappingException("Cannot use field - [{$slugField}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->name}"); - } - } - - $config['slugs'][$field]['fields'] = $slug['fields']; - $config['slugs'][$field]['handlers'] = $handlers; - $config['slugs'][$field]['slug'] = $field; - $config['slugs'][$field]['style'] = isset($slug['style']) ? - (string) $slug['style'] : 'default'; - - $config['slugs'][$field]['dateFormat'] = isset($slug['dateFormat']) ? - (string) $slug['dateFormat'] : 'Y-m-d-H:i'; - - $config['slugs'][$field]['updatable'] = isset($slug['updatable']) ? - (bool) $slug['updatable'] : true; - - $config['slugs'][$field]['unique'] = isset($slug['unique']) ? - (bool) $slug['unique'] : true; - - $config['slugs'][$field]['unique_base'] = isset($slug['unique_base']) ? - $slug['unique_base'] : null; - - $config['slugs'][$field]['separator'] = isset($slug['separator']) ? - (string) $slug['separator'] : '-'; - - $config['slugs'][$field]['prefix'] = isset($slug['prefix']) ? - (string) $slug['prefix'] : ''; - - $config['slugs'][$field]['suffix'] = isset($slug['suffix']) ? - (string) $slug['suffix'] : ''; - - if (!$meta->isMappedSuperclass && $meta->isIdentifier($field) && !$config['slugs'][$field]['unique']) { - throw new InvalidMappingException("Identifier field - [{$field}] slug must be unique in order to maintain primary key in class - {$meta->name}"); - } - $ubase = $config['slugs'][$field]['unique_base']; - if ($config['slugs'][$field]['unique'] === false && $ubase) { - throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'"); - } - if ($ubase && !$meta->hasField($ubase) && !$meta->hasAssociation($ubase)) { - throw new InvalidMappingException("Unable to find [{$ubase}] as mapped property in entity - {$meta->name}"); - } - } - } - } - } - } - - /** - * {@inheritDoc} - */ - protected function _loadMappingFile($file) - { - return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); - } - - /** - * Checks if $field type is valid as Sluggable field - * - * @param object $meta - * @param string $field - * - * @return boolean - */ - protected function isValidField($meta, $field) - { - $mapping = $meta->getFieldMapping($field); - - return $mapping && in_array($mapping['type'], $this->validTypes); - } -} diff --git a/lib/Gedmo/Sluggable/Mapping/Event/SluggableAdapter.php b/lib/Gedmo/Sluggable/Mapping/Event/SluggableAdapter.php deleted file mode 100644 index 4882b801e9..0000000000 --- a/lib/Gedmo/Sluggable/Mapping/Event/SluggableAdapter.php +++ /dev/null @@ -1,54 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface SluggableAdapter extends AdapterInterface -{ - /** - * Loads the similar slugs - * - * @param object $object - * @param object $meta - * @param array $config - * @param string $slug - * - * @return array - */ - public function getSimilarSlugs($object, $meta, array $config, $slug); - - /** - * Replace part of slug to all objects - * matching $target pattern - * - * @param object $object - * @param array $config - * @param string $target - * @param string $replacement - * - * @return integer - */ - public function replaceRelative($object, array $config, $target, $replacement); - - /** - * Replace part of slug to all objects - * matching $target pattern and having $object - * related - * - * @param object $object - * @param array $config - * @param string $target - * @param string $replacement - * - * @return integer - */ - public function replaceInverseRelative($object, array $config, $target, $replacement); -} diff --git a/lib/Gedmo/Sluggable/Util/Urlizer.php b/lib/Gedmo/Sluggable/Util/Urlizer.php deleted file mode 100644 index f3331e9531..0000000000 --- a/lib/Gedmo/Sluggable/Util/Urlizer.php +++ /dev/null @@ -1,12 +0,0 @@ -getName(); - if (array_key_exists($class, $this->disabled) && $this->disabled[$class] === true) { - return array(); - } elseif (array_key_exists($targetEntity->rootDocumentName, $this->disabled) && $this->disabled[$targetEntity->rootDocumentName] === true) { - return array(); - } - - $config = $this->getListener()->getConfiguration($this->getDocumentManager(), $targetEntity->name); - - if (!isset($config['softDeleteable']) || !$config['softDeleteable']) { - return array(); - } - - $column = $targetEntity->fieldMappings[$config['fieldName']]; - - if (isset($config['timeAware']) && $config['timeAware']) { - return array( - '$or' => array( - array($column['fieldName'] => null), - array($column['fieldName'] => array('$gt' => new \DateTime('now'))), - ), - ); - } - - return array( - $column['fieldName'] => null, - ); - } - - protected function getListener() - { - if ($this->listener === null) { - $em = $this->getDocumentManager(); - $evm = $em->getEventManager(); - - foreach ($evm->getListeners() as $listeners) { - foreach ($listeners as $listener) { - if ($listener instanceof SoftDeleteableListener) { - $this->listener = $listener; - - break 2; - } - } - } - - if ($this->listener === null) { - throw new \RuntimeException('Listener "SoftDeleteableListener" was not added to the EventManager!'); - } - } - - return $this->listener; - } - - protected function getDocumentManager() - { - if ($this->documentManager === null) { - $refl = new \ReflectionProperty('Doctrine\ODM\MongoDB\Query\Filter\BsonFilter', 'dm'); - $refl->setAccessible(true); - $this->documentManager = $refl->getValue($this); - } - - return $this->documentManager; - } - - public function disableForDocument($class) - { - $this->disabled[$class] = true; - } - - public function enableForDocument($class) - { - $this->disabled[$class] = false; - } -} diff --git a/lib/Gedmo/SoftDeleteable/Filter/SoftDeleteableFilter.php b/lib/Gedmo/SoftDeleteable/Filter/SoftDeleteableFilter.php deleted file mode 100644 index 1c35150729..0000000000 --- a/lib/Gedmo/SoftDeleteable/Filter/SoftDeleteableFilter.php +++ /dev/null @@ -1,97 +0,0 @@ - - * @author Gediminas Morkevicius - * @author Patrik Votoฤek - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ - -class SoftDeleteableFilter extends SQLFilter -{ - protected $listener; - protected $entityManager; - protected $disabled = array(); - - public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) - { - $class = $targetEntity->getName(); - if (array_key_exists($class, $this->disabled) && $this->disabled[$class] === true) { - return ''; - } elseif (array_key_exists($targetEntity->rootEntityName, $this->disabled) && $this->disabled[$targetEntity->rootEntityName] === true) { - return ''; - } - - $config = $this->getListener()->getConfiguration($this->getEntityManager(), $targetEntity->name); - - if (!isset($config['softDeleteable']) || !$config['softDeleteable']) { - return ''; - } - - $conn = $this->getEntityManager()->getConnection(); - $platform = $conn->getDatabasePlatform(); - $column = $targetEntity->getQuotedColumnName($config['fieldName'], $platform); - - $addCondSql = $platform->getIsNullExpression($targetTableAlias.'.'.$column); - if (isset($config['timeAware']) && $config['timeAware']) { - $now = $conn->quote(date($platform->getDateTimeFormatString())); // should use UTC in database and PHP - $addCondSql = "({$addCondSql} OR {$targetTableAlias}.{$column} > {$now})"; - } - - return $addCondSql; - } - - public function disableForEntity($class) - { - $this->disabled[$class] = true; - } - - public function enableForEntity($class) - { - $this->disabled[$class] = false; - } - - protected function getListener() - { - if ($this->listener === null) { - $em = $this->getEntityManager(); - $evm = $em->getEventManager(); - - foreach ($evm->getListeners() as $listeners) { - foreach ($listeners as $listener) { - if ($listener instanceof SoftDeleteableListener) { - $this->listener = $listener; - - break 2; - } - } - } - - if ($this->listener === null) { - throw new \RuntimeException('Listener "SoftDeleteableListener" was not added to the EventManager!'); - } - } - - return $this->listener; - } - - protected function getEntityManager() - { - if ($this->entityManager === null) { - $refl = new \ReflectionProperty('Doctrine\ORM\Query\Filter\SQLFilter', 'em'); - $refl->setAccessible(true); - $this->entityManager = $refl->getValue($this); - } - - return $this->entityManager; - } -} diff --git a/lib/Gedmo/SoftDeleteable/Mapping/Driver/Annotation.php b/lib/Gedmo/SoftDeleteable/Mapping/Driver/Annotation.php deleted file mode 100644 index 4845fa6379..0000000000 --- a/lib/Gedmo/SoftDeleteable/Mapping/Driver/Annotation.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Annotation extends AbstractAnnotationDriver -{ - /** - * Annotation to define that this object is loggable - */ - const SOFT_DELETEABLE = 'Gedmo\\Mapping\\Annotation\\SoftDeleteable'; - - /** - * {@inheritDoc} - */ - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - // class annotations - if ($class !== null && $annot = $this->reader->getClassAnnotation($class, self::SOFT_DELETEABLE)) { - $config['softDeleteable'] = true; - - Validator::validateField($meta, $annot->fieldName); - - $config['fieldName'] = $annot->fieldName; - - $config['timeAware'] = false; - if (isset($annot->timeAware)) { - if (!is_bool($annot->timeAware)) { - throw new InvalidMappingException("timeAware must be boolean. ".gettype($annot->timeAware)." provided."); - } - $config['timeAware'] = $annot->timeAware; - } - } - - $this->validateFullMetadata($meta, $config); - } -} diff --git a/lib/Gedmo/SoftDeleteable/Mapping/Event/Adapter/ORM.php b/lib/Gedmo/SoftDeleteable/Mapping/Event/Adapter/ORM.php deleted file mode 100644 index c1e721e224..0000000000 --- a/lib/Gedmo/SoftDeleteable/Mapping/Event/Adapter/ORM.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class ORM extends BaseAdapterORM implements SoftDeleteableAdapter -{ -} diff --git a/lib/Gedmo/SoftDeleteable/Mapping/Event/SoftDeleteableAdapter.php b/lib/Gedmo/SoftDeleteable/Mapping/Event/SoftDeleteableAdapter.php deleted file mode 100644 index 6a67e4f59c..0000000000 --- a/lib/Gedmo/SoftDeleteable/Mapping/Event/SoftDeleteableAdapter.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface SoftDeleteableAdapter extends AdapterInterface -{ -} diff --git a/lib/Gedmo/SoftDeleteable/Mapping/Validator.php b/lib/Gedmo/SoftDeleteable/Mapping/Validator.php deleted file mode 100644 index 6b9f741cc6..0000000000 --- a/lib/Gedmo/SoftDeleteable/Mapping/Validator.php +++ /dev/null @@ -1,46 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ - -class Validator -{ - /** - * List of types which are valid for timestamp - * - * @var array - */ - public static $validTypes = array( - 'date', - 'time', - 'datetime', - 'datetimetz', - 'timestamp', - 'zenddate', - ); - - public static function validateField(ClassMetadata $meta, $field) - { - if ($meta->isMappedSuperclass) { - return; - } - - $fieldMapping = $meta->getFieldMapping($field); - - if (!in_array($fieldMapping['type'], self::$validTypes)) { - throw new InvalidMappingException(sprintf('Field "%s" must be of one of the following types: "%s"', - $fieldMapping['type'], - implode(', ', self::$validTypes))); - } - } -} diff --git a/lib/Gedmo/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php b/lib/Gedmo/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php deleted file mode 100644 index 9d89854ee6..0000000000 --- a/lib/Gedmo/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ - -class MultiTableDeleteExecutor extends BaseMultiTableDeleteExecutor -{ - /** - * {@inheritDoc} - */ - public function __construct(Node $AST, $sqlWalker, ClassMetadataInfo $meta, AbstractPlatform $platform, array $config) - { - parent::__construct($AST, $sqlWalker); - - $reflProp = new \ReflectionProperty(get_class($this), '_sqlStatements'); - $reflProp->setAccessible(true); - - $sqlStatements = $reflProp->getValue($this); - - foreach ($sqlStatements as $index => $stmt) { - $matches = array(); - preg_match('/DELETE FROM (\w+) .+/', $stmt, $matches); - - if (isset($matches[1]) && $meta->getQuotedTableName($platform) === $matches[1]) { - $sqlStatements[$index] = str_replace('DELETE FROM', 'UPDATE', $stmt); - $sqlStatements[$index] = str_replace('WHERE', 'SET '.$config['fieldName'].' = "'.date('Y-m-d H:i:s').'" WHERE', $sqlStatements[$index]); - } else { - // We have to avoid the removal of registers of child entities of a SoftDeleteable entity - unset($sqlStatements[$index]); - } - } - - $reflProp->setValue($this, $sqlStatements); - } -} diff --git a/lib/Gedmo/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php b/lib/Gedmo/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php deleted file mode 100644 index bfa8615453..0000000000 --- a/lib/Gedmo/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php +++ /dev/null @@ -1,141 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ - -class SoftDeleteableWalker extends SqlWalker -{ - protected $conn; - protected $platform; - protected $listener; - protected $configuration; - protected $alias; - protected $deletedAtField; - protected $meta; - - /** - * {@inheritDoc} - */ - public function __construct($query, $parserResult, array $queryComponents) - { - parent::__construct($query, $parserResult, $queryComponents); - - $this->conn = $this->getConnection(); - $this->platform = $this->conn->getDatabasePlatform(); - $this->listener = $this->getSoftDeleteableListener(); - $this->extractComponents($queryComponents); - } - - /** - * {@inheritDoc} - */ - public function getExecutor($AST) - { - switch (true) { - case ($AST instanceof DeleteStatement): - $primaryClass = $this->getEntityManager()->getClassMetadata($AST->deleteClause->abstractSchemaName); - - return ($primaryClass->isInheritanceTypeJoined()) - ? new MultiTableDeleteExecutor($AST, $this, $this->meta, $this->platform, $this->configuration) - : new SingleTableDeleteUpdateExecutor($AST, $this); - default: - throw new \Gedmo\Exception\UnexpectedValueException('SoftDeleteable walker should be used only on delete statement'); - } - } - - /** - * Change a DELETE clause for an UPDATE clause - * - * @param DeleteClause $deleteClause - * - * @return string The SQL. - */ - public function walkDeleteClause(DeleteClause $deleteClause) - { - $em = $this->getEntityManager(); - $class = $em->getClassMetadata($deleteClause->abstractSchemaName); - $tableName = $class->getTableName(); - $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable); - $quotedTableName = $class->getQuotedTableName($this->platform); - $quotedColumnName = $class->getQuotedColumnName($this->deletedAtField, $this->platform); - - $sql = 'UPDATE '.$quotedTableName.' SET '.$quotedColumnName.' = '.$this->conn->quote(date( - $this->platform->getDateTimeFormatString() - )); - - return $sql; - } - - /** - * Get the currently used SoftDeleteableListener - * - * @throws \Gedmo\Exception\RuntimeException - if listener is not found - * - * @return SoftDeleteableListener - */ - private function getSoftDeleteableListener() - { - if (is_null($this->listener)) { - $em = $this->getEntityManager(); - - foreach ($em->getEventManager()->getListeners() as $event => $listeners) { - foreach ($listeners as $hash => $listener) { - if ($listener instanceof SoftDeleteableListener) { - $this->listener = $listener; - break; - } - } - if ($this->listener) { - break; - } - } - - if (is_null($this->listener)) { - throw new \Gedmo\Exception\RuntimeException('The SoftDeleteable listener could not be found.'); - } - } - - return $this->listener; - } - - /** - * Search for components in the delete clause - * - * @param array $queryComponents - * - * @return void - */ - private function extractComponents(array $queryComponents) - { - $em = $this->getEntityManager(); - - foreach ($queryComponents as $alias => $comp) { - if (!isset($comp['metadata'])) { - continue; - } - $meta = $comp['metadata']; - $config = $this->listener->getConfiguration($em, $meta->name); - if ($config && isset($config['softDeleteable']) && $config['softDeleteable']) { - $this->configuration = $config; - $this->deletedAtField = $config['fieldName']; - $this->meta = $meta; - } - } - } -} diff --git a/lib/Gedmo/SoftDeleteable/SoftDeleteable.php b/lib/Gedmo/SoftDeleteable/SoftDeleteable.php deleted file mode 100644 index 6faa3d004c..0000000000 --- a/lib/Gedmo/SoftDeleteable/SoftDeleteable.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface SoftDeleteable -{ - // this interface is not necessary to implement - - /** - * @gedmo:SoftDeleteable - * to mark the class as SoftDeleteable use class annotation @gedmo:SoftDeleteable - * this object will be able to be soft deleted - * example: - * - * @gedmo:SoftDeleteable - * class MyEntity - */ -} diff --git a/lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php b/lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php deleted file mode 100644 index 3dd56808c5..0000000000 --- a/lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php +++ /dev/null @@ -1,116 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class SoftDeleteableListener extends MappedEventSubscriber -{ - /** - * Pre soft-delete event - * - * @var string - */ - const PRE_SOFT_DELETE = "preSoftDelete"; - - /** - * Post soft-delete event - * - * @var string - */ - const POST_SOFT_DELETE = "postSoftDelete"; - - /** - * {@inheritdoc} - */ - public function getSubscribedEvents() - { - return array( - 'loadClassMetadata', - 'onFlush', - ); - } - - /** - * If it's a SoftDeleteable object, update the "deletedAt" field - * and skip the removal of the object - * - * @param EventArgs $args - * - * @return void - */ - public function onFlush(EventArgs $args) - { - $ea = $this->getEventAdapter($args); - $om = $ea->getObjectManager(); - $uow = $om->getUnitOfWork(); - $evm = $om->getEventManager(); - - //getScheduledDocumentDeletions - foreach ($ea->getScheduledObjectDeletions($uow) as $object) { - $meta = $om->getClassMetadata(get_class($object)); - $config = $this->getConfiguration($om, $meta->name); - - if (isset($config['softDeleteable']) && $config['softDeleteable']) { - $reflProp = $meta->getReflectionProperty($config['fieldName']); - $oldValue = $reflProp->getValue($object); - if ($oldValue instanceof \Datetime) { - continue; // want to hard delete - } - - $evm->dispatchEvent( - self::PRE_SOFT_DELETE, - $ea->createLifecycleEventArgsInstance($object, $om) - ); - - $date = new \DateTime(); - $reflProp->setValue($object, $date); - - $om->persist($object); - $uow->propertyChanged($object, $config['fieldName'], $oldValue, $date); - if ($uow instanceof MongoDBUnitOfWork && !method_exists($uow, 'scheduleExtraUpdate')) { - $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); - } else { - $uow->scheduleExtraUpdate($object, array( - $config['fieldName'] => array($oldValue, $date), - )); - } - - $evm->dispatchEvent( - self::POST_SOFT_DELETE, - $ea->createLifecycleEventArgsInstance($object, $om) - ); - } - } - } - - /** - * Maps additional metadata - * - * @param EventArgs $eventArgs - * - * @return void - */ - public function loadClassMetadata(EventArgs $eventArgs) - { - $ea = $this->getEventAdapter($eventArgs); - $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata()); - } - - /** - * {@inheritDoc} - */ - protected function getNamespace() - { - return __NAMESPACE__; - } -} diff --git a/lib/Gedmo/SoftDeleteable/Traits/SoftDeleteable.php b/lib/Gedmo/SoftDeleteable/Traits/SoftDeleteable.php deleted file mode 100644 index 7918d21398..0000000000 --- a/lib/Gedmo/SoftDeleteable/Traits/SoftDeleteable.php +++ /dev/null @@ -1,51 +0,0 @@ -= 5.4 - * - * @author Wesley van Opdorp - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -trait SoftDeleteable -{ - /** - * @var \DateTime - */ - protected $deletedAt; - - /** - * Sets deletedAt. - * - * @param \Datetime|null $deletedAt - * - * @return $this - */ - public function setDeletedAt(\DateTime $deletedAt = null) - { - $this->deletedAt = $deletedAt; - - return $this; - } - - /** - * Returns deletedAt. - * - * @return \DateTime - */ - public function getDeletedAt() - { - return $this->deletedAt; - } - - /** - * Is deleted? - * - * @return bool - */ - public function isDeleted() - { - return null !== $this->deletedAt; - } -} diff --git a/lib/Gedmo/SoftDeleteable/Traits/SoftDeleteableDocument.php b/lib/Gedmo/SoftDeleteable/Traits/SoftDeleteableDocument.php deleted file mode 100644 index c283a6819c..0000000000 --- a/lib/Gedmo/SoftDeleteable/Traits/SoftDeleteableDocument.php +++ /dev/null @@ -1,54 +0,0 @@ -= 5.4 - * - * @author Wesley van Opdorp - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -trait SoftDeleteableDocument -{ - /** - * @var \DateTime - * @ODM\Date - */ - protected $deletedAt; - - /** - * Sets deletedAt. - * - * @param \Datetime|null $deletedAt - * - * @return $this - */ - public function setDeletedAt(\DateTime $deletedAt = null) - { - $this->deletedAt = $deletedAt; - - return $this; - } - - /** - * Returns deletedAt. - * - * @return \DateTime - */ - public function getDeletedAt() - { - return $this->deletedAt; - } - - /** - * Is deleted? - * - * @return bool - */ - public function isDeleted() - { - return null !== $this->deletedAt; - } -} diff --git a/lib/Gedmo/SoftDeleteable/Traits/SoftDeleteableEntity.php b/lib/Gedmo/SoftDeleteable/Traits/SoftDeleteableEntity.php deleted file mode 100644 index d82fcf4471..0000000000 --- a/lib/Gedmo/SoftDeleteable/Traits/SoftDeleteableEntity.php +++ /dev/null @@ -1,54 +0,0 @@ -= 5.4 - * - * @author Wesley van Opdorp - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -trait SoftDeleteableEntity -{ - /** - * @var \DateTime - * @ORM\Column(type="datetime", nullable=true) - */ - protected $deletedAt; - - /** - * Sets deletedAt. - * - * @param \Datetime|null $deletedAt - * - * @return $this - */ - public function setDeletedAt(\DateTime $deletedAt = null) - { - $this->deletedAt = $deletedAt; - - return $this; - } - - /** - * Returns deletedAt. - * - * @return \DateTime - */ - public function getDeletedAt() - { - return $this->deletedAt; - } - - /** - * Is deleted? - * - * @return bool - */ - public function isDeleted() - { - return null !== $this->deletedAt; - } -} diff --git a/lib/Gedmo/Sortable/Entity/Repository/SortableRepository.php b/lib/Gedmo/Sortable/Entity/Repository/SortableRepository.php deleted file mode 100644 index d878613618..0000000000 --- a/lib/Gedmo/Sortable/Entity/Repository/SortableRepository.php +++ /dev/null @@ -1,91 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class SortableRepository extends EntityRepository -{ - /** - * Sortable listener on event manager - * - * @var SortableListener - */ - protected $listener = null; - - protected $config = null; - protected $meta = null; - - public function __construct(EntityManager $em, ClassMetadata $class) - { - parent::__construct($em, $class); - $sortableListener = null; - foreach ($em->getEventManager()->getListeners() as $event => $listeners) { - foreach ($listeners as $hash => $listener) { - if ($listener instanceof SortableListener) { - $sortableListener = $listener; - break; - } - } - if ($sortableListener) { - break; - } - } - - if (is_null($sortableListener)) { - throw new \Gedmo\Exception\InvalidMappingException('This repository can be attached only to ORM sortable listener'); - } - - $this->listener = $sortableListener; - $this->meta = $this->getClassMetadata(); - $this->config = $this->listener->getConfiguration($this->_em, $this->meta->name); - } - - public function getBySortableGroupsQuery(array $groupValues = array()) - { - return $this->getBySortableGroupsQueryBuilder($groupValues)->getQuery(); - } - - public function getBySortableGroupsQueryBuilder(array $groupValues = array()) - { - $groups = isset($this->config['groups']) ? array_combine(array_values($this->config['groups']), array_keys($this->config['groups'])) : array(); - foreach ($groupValues as $name => $value) { - if (!in_array($name, $this->config['groups'])) { - throw new \InvalidArgumentException('Sortable group "'.$name.'" is not defined in Entity '.$this->meta->name); - } - unset($groups[$name]); - } - if (count($groups) > 0) { - throw new \InvalidArgumentException( - 'You need to specify values for the following groups to select by sortable groups: '.implode(", ", array_keys($groups))); - } - - $qb = $this->createQueryBuilder('n'); - $qb->orderBy('n.'.$this->config['position']); - $i = 1; - foreach ($groupValues as $group => $value) { - $qb->andWhere('n.'.$group.' = :group'.$i) - ->setParameter('group'.$i, $value); - $i++; - } - - return $qb; - } - - public function getBySortableGroups(array $groupValues = array()) - { - $query = $this->getBySortableGroupsQuery($groupValues); - - return $query->getResult(); - } - -} diff --git a/lib/Gedmo/Sortable/Mapping/Event/SortableAdapter.php b/lib/Gedmo/Sortable/Mapping/Event/SortableAdapter.php deleted file mode 100644 index dba55cffb7..0000000000 --- a/lib/Gedmo/Sortable/Mapping/Event/SortableAdapter.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface SortableAdapter extends AdapterInterface -{ -} diff --git a/lib/Gedmo/Sortable/Sortable.php b/lib/Gedmo/Sortable/Sortable.php deleted file mode 100644 index 6e757dca84..0000000000 --- a/lib/Gedmo/Sortable/Sortable.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface Sortable -{ - // use now annotations instead of predefined methods, this interface is not necessary - - /** - * @gedmo:SortablePosition - to mark property which will hold the item position use annotation @gedmo:SortablePosition - * This property has to be numeric. The position index can be negative and will be counted from right to left. - * - * example: - * - * @gedmo:SortablePosition - * @Column(type="int") - * $position - * - * @gedmo:SortableGroup - * @Column(type="string", length=64) - * $category - * - */ - - /** - * @gedmo:SortableGroup - to group node sorting by a property use annotation @gedmo:SortableGroup on this property - * - * example: - * - * @gedmo:SortableGroup - * @Column(type="string", length=64) - * $category - */ -} diff --git a/lib/Gedmo/Timestampable/Mapping/Event/Adapter/ODM.php b/lib/Gedmo/Timestampable/Mapping/Event/Adapter/ODM.php deleted file mode 100644 index 1fd4bc0780..0000000000 --- a/lib/Gedmo/Timestampable/Mapping/Event/Adapter/ODM.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class ODM extends BaseAdapterODM implements TimestampableAdapter -{ - /** - * {@inheritDoc} - */ - public function getDateValue($meta, $field) - { - $mapping = $meta->getFieldMapping($field); - if (isset($mapping['type']) && $mapping['type'] === 'timestamp') { - return time(); - } - if (isset($mapping['type']) && $mapping['type'] == 'zenddate') { - return new \Zend_Date(); - } - - return \DateTime::createFromFormat('U.u', number_format(microtime(true), 6, '.', '')) - ->setTimeZone(new \DateTimeZone(date_default_timezone_get())); - } -} diff --git a/lib/Gedmo/Timestampable/Mapping/Event/Adapter/ORM.php b/lib/Gedmo/Timestampable/Mapping/Event/Adapter/ORM.php deleted file mode 100644 index 6285674c3f..0000000000 --- a/lib/Gedmo/Timestampable/Mapping/Event/Adapter/ORM.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -final class ORM extends BaseAdapterORM implements TimestampableAdapter -{ - /** - * {@inheritDoc} - */ - public function getDateValue($meta, $field) - { - $mapping = $meta->getFieldMapping($field); - if (isset($mapping['type']) && $mapping['type'] === 'integer') { - return time(); - } - if (isset($mapping['type']) && $mapping['type'] == 'zenddate') { - return new \Zend_Date(); - } - - return \DateTime::createFromFormat('U.u', number_format(microtime(true), 6, '.', '')) - ->setTimeZone(new \DateTimeZone(date_default_timezone_get())); - } -} diff --git a/lib/Gedmo/Timestampable/Mapping/Event/TimestampableAdapter.php b/lib/Gedmo/Timestampable/Mapping/Event/TimestampableAdapter.php deleted file mode 100644 index 48005cdfa7..0000000000 --- a/lib/Gedmo/Timestampable/Mapping/Event/TimestampableAdapter.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface TimestampableAdapter extends AdapterInterface -{ - /** - * Get the date value - * - * @param object $meta - * @param string $field - * - * @return mixed - */ - public function getDateValue($meta, $field); -} diff --git a/lib/Gedmo/Timestampable/Timestampable.php b/lib/Gedmo/Timestampable/Timestampable.php deleted file mode 100644 index 6c3ff15497..0000000000 --- a/lib/Gedmo/Timestampable/Timestampable.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface Timestampable -{ - // timestampable expects annotations on properties - - /** - * @gedmo:Timestampable(on="create") - * dates which should be updated on insert only - */ - - /** - * @gedmo:Timestampable(on="update") - * dates which should be updated on update and insert - */ - - /** - * @gedmo:Timestampable(on="change", field="field", value="value") - * dates which should be updated on changed "property" - * value and become equal to given "value" - */ - - /** - * @gedmo:Timestampable(on="change", field="field") - * dates which should be updated on changed "property" - */ - - /** - * @gedmo:Timestampable(on="change", fields={"field1", "field2"}) - * dates which should be updated if at least one of the given fields changed - */ - - /** - * example - * - * @gedmo:Timestampable(on="create") - * @Column(type="date") - * $created - */ -} diff --git a/lib/Gedmo/Tool/Wrapper/AbstractWrapper.php b/lib/Gedmo/Tool/Wrapper/AbstractWrapper.php deleted file mode 100644 index 9e55a83eab..0000000000 --- a/lib/Gedmo/Tool/Wrapper/AbstractWrapper.php +++ /dev/null @@ -1,100 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -abstract class AbstractWrapper implements WrapperInterface -{ - /** - * Object metadata - * - * @var object - */ - protected $meta; - - /** - * Wrapped object - * - * @var object - */ - protected $object; - - /** - * Object manager instance - * - * @var \Doctrine\Common\Persistence\ObjectManager - */ - protected $om; - - /** - * List of wrapped object references - * - * @var array - */ - private static $wrappedObjectReferences; - - /** - * Wrap object factory method - * - * @param object $object - * @param ObjectManager $om - * - * @throws \Gedmo\Exception\UnsupportedObjectManagerException - * - * @return \Gedmo\Tool\WrapperInterface - */ - public static function wrap($object, ObjectManager $om) - { - if ($om instanceof EntityManager) { - return new EntityWrapper($object, $om); - } elseif ($om instanceof DocumentManager) { - return new MongoDocumentWrapper($object, $om); - } - throw new UnsupportedObjectManagerException('Given object manager is not managed by wrapper'); - } - - public static function clear() - { - self::$wrappedObjectReferences = array(); - } - - /** - * {@inheritDoc} - */ - public function getObject() - { - return $this->object; - } - - /** - * {@inheritDoc} - */ - public function getMetadata() - { - return $this->meta; - } - - /** - * {@inheritDoc} - */ - public function populate(array $data) - { - foreach ($data as $field => $value) { - $this->setPropertyValue($field, $value); - } - - return $this; - } -} diff --git a/lib/Gedmo/Tool/Wrapper/EntityWrapper.php b/lib/Gedmo/Tool/Wrapper/EntityWrapper.php deleted file mode 100644 index 4d9255eefc..0000000000 --- a/lib/Gedmo/Tool/Wrapper/EntityWrapper.php +++ /dev/null @@ -1,138 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class EntityWrapper extends AbstractWrapper -{ - /** - * Entity identifier - * - * @var array - */ - private $identifier; - - /** - * True if entity or proxy is loaded - * - * @var boolean - */ - private $initialized = false; - - /** - * Wrap entity - * - * @param object $entity - * @param \Doctrine\ORM\EntityManager $em - */ - public function __construct($entity, EntityManager $em) - { - $this->om = $em; - $this->object = $entity; - $this->meta = $em->getClassMetadata(get_class($this->object)); - } - - /** - * {@inheritDoc} - */ - public function getPropertyValue($property) - { - $this->initialize(); - - return $this->meta->getReflectionProperty($property)->getValue($this->object); - } - - /** - * {@inheritDoc} - */ - public function setPropertyValue($property, $value) - { - $this->initialize(); - $this->meta->getReflectionProperty($property)->setValue($this->object, $value); - - return $this; - } - - /** - * {@inheritDoc} - */ - public function hasValidIdentifier() - { - return (null !== $this->getIdentifier()); - } - - /** - * {@inheritDoc} - */ - public function getRootObjectName() - { - return $this->meta->rootEntityName; - } - - /** - * {@inheritDoc} - */ - public function getIdentifier($single = true) - { - if (null === $this->identifier) { - if ($this->object instanceof Proxy) { - $uow = $this->om->getUnitOfWork(); - if ($uow->isInIdentityMap($this->object)) { - $this->identifier = $uow->getEntityIdentifier($this->object); - } else { - $this->initialize(); - } - } - if (null === $this->identifier) { - $this->identifier = array(); - $incomplete = false; - foreach ($this->meta->identifier as $name) { - $this->identifier[$name] = $this->getPropertyValue($name); - if (null === $this->identifier[$name]) { - $incomplete = true; - } - } - if ($incomplete) { - $this->identifier = null; - } - } - } - if ($single && is_array($this->identifier)) { - return reset($this->identifier); - } - - return $this->identifier; - } - - /** - * Initialize the entity if it is proxy - * required when is detached or not initialized - */ - protected function initialize() - { - if (!$this->initialized) { - if ($this->object instanceof Proxy) { - if (!$this->object->__isInitialized__) { - $this->object->__load(); - } - } - } - } - - /** - * {@inheritDoc} - */ - public function isEmbeddedAssociation($field) - { - return false; - } -} diff --git a/lib/Gedmo/Tool/WrapperInterface.php b/lib/Gedmo/Tool/WrapperInterface.php deleted file mode 100644 index adde4573e9..0000000000 --- a/lib/Gedmo/Tool/WrapperInterface.php +++ /dev/null @@ -1,87 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface WrapperInterface -{ - /** - * Get currently wrapped object - * etc.: entity, document - * - * @return object - */ - public function getObject(); - - /** - * Extract property value from object - * - * @param string $property - * - * @return mixed - */ - public function getPropertyValue($property); - - /** - * Set the property - * - * @param string $property - * @param mixed $value - * - * @return \Gedmo\Tool\WrapperInterface - */ - public function setPropertyValue($property, $value); - - /** - * Populates the object with given property values - * - * @param array $data - * - * @return static - */ - public function populate(array $data); - - /** - * Checks if identifier is valid - * - * @return boolean - */ - public function hasValidIdentifier(); - - /** - * Get metadata - * - * @return object - */ - public function getMetadata(); - - /** - * Get the object identifier, single or composite - * - * @param boolean $single - * - * @return array|mixed - */ - public function getIdentifier($single = true); - - /** - * Get root object class name - * - * @return string - */ - public function getRootObjectName(); - - /** - * Chechks if association is embedded - * - * @param string $field - * - * @return bool - */ - public function isEmbeddedAssociation($field); -} diff --git a/lib/Gedmo/Translatable/Document/Translation.php b/lib/Gedmo/Translatable/Document/Translation.php deleted file mode 100644 index a82a444a1d..0000000000 --- a/lib/Gedmo/Translatable/Document/Translation.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class TranslationRepository extends EntityRepository -{ - /** - * Current TranslatableListener instance used - * in EntityManager - * - * @var TranslatableListener - */ - private $listener; - - /** - * {@inheritdoc} - */ - public function __construct(EntityManager $em, ClassMetadata $class) - { - if ($class->getReflectionClass()->isSubclassOf('Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation')) { - throw new \Gedmo\Exception\UnexpectedValueException('This repository is useless for personal translations'); - } - parent::__construct($em, $class); - } - - /** - * Makes additional translation of $entity $field into $locale - * using $value - * - * @param object $entity - * @param string $field - * @param string $locale - * @param mixed $value - * - * @throws \Gedmo\Exception\InvalidArgumentException - * - * @return static - */ - public function translate($entity, $field, $locale, $value) - { - $meta = $this->_em->getClassMetadata(get_class($entity)); - $listener = $this->getTranslatableListener(); - $config = $listener->getConfiguration($this->_em, $meta->name); - if (!isset($config['fields']) || !in_array($field, $config['fields'])) { - throw new \Gedmo\Exception\InvalidArgumentException("Entity: {$meta->name} does not translate field - {$field}"); - } - $needsPersist = true; - if ($locale === $listener->getTranslatableLocale($entity, $meta, $this->getEntityManager())) { - $meta->getReflectionProperty($field)->setValue($entity, $value); - $this->_em->persist($entity); - } else { - if (isset($config['translationClass'])) { - $class = $config['translationClass']; - } else { - $ea = new TranslatableAdapterORM(); - $class = $listener->getTranslationClass($ea, $config['useObjectClass']); - } - $foreignKey = $meta->getReflectionProperty($meta->getSingleIdentifierFieldName())->getValue($entity); - $objectClass = $config['useObjectClass']; - $transMeta = $this->_em->getClassMetadata($class); - $trans = $this->findOneBy(compact('locale', 'objectClass', 'field', 'foreignKey')); - if (!$trans) { - $trans = $transMeta->newInstance(); - $transMeta->getReflectionProperty('foreignKey')->setValue($trans, $foreignKey); - $transMeta->getReflectionProperty('objectClass')->setValue($trans, $objectClass); - $transMeta->getReflectionProperty('field')->setValue($trans, $field); - $transMeta->getReflectionProperty('locale')->setValue($trans, $locale); - } - if ($listener->getDefaultLocale() != $listener->getTranslatableLocale($entity, $meta, $this->getEntityManager()) && - $locale === $listener->getDefaultLocale()) { - $listener->setTranslationInDefaultLocale(spl_object_hash($entity), $field, $trans); - $needsPersist = $listener->getPersistDefaultLocaleTranslation(); - } - $type = Type::getType($meta->getTypeOfField($field)); - $transformed = $type->convertToDatabaseValue($value, $this->_em->getConnection()->getDatabasePlatform()); - $transMeta->getReflectionProperty('content')->setValue($trans, $transformed); - if ($needsPersist) { - if ($this->_em->getUnitOfWork()->isInIdentityMap($entity)) { - $this->_em->persist($trans); - } else { - $oid = spl_object_hash($entity); - $listener->addPendingTranslationInsert($oid, $trans); - } - } - } - - return $this; - } - - /** - * Loads all translations with all translatable - * fields from the given entity - * - * @param object $entity Must implement Translatable - * - * @return array list of translations in locale groups - */ - public function findTranslations($entity) - { - $result = array(); - $wrapped = new EntityWrapper($entity, $this->_em); - if ($wrapped->hasValidIdentifier()) { - $entityId = $wrapped->getIdentifier(); - $entityClass = $wrapped->getMetadata()->rootEntityName; - $translationMeta = $this->getClassMetadata(); // table inheritance support - - $config = $this - ->getTranslatableListener() - ->getConfiguration($this->_em, get_class($entity)); - - $translationClass = isset($config['translationClass']) ? - $config['translationClass'] : - $translationMeta->rootEntityName; - - $qb = $this->_em->createQueryBuilder(); - $qb->select('trans.content, trans.field, trans.locale') - ->from($translationClass, 'trans') - ->where('trans.foreignKey = :entityId', 'trans.objectClass = :entityClass') - ->orderBy('trans.locale'); - $q = $qb->getQuery(); - $data = $q->execute( - compact('entityId', 'entityClass'), - Query::HYDRATE_ARRAY - ); - - if ($data && is_array($data) && count($data)) { - foreach ($data as $row) { - $result[$row['locale']][$row['field']] = $row['content']; - } - } - } - - return $result; - } - - /** - * Find the entity $class by the translated field. - * Result is the first occurrence of translated field. - * Query can be slow, since there are no indexes on such - * columns - * - * @param string $field - * @param string $value - * @param string $class - * - * @return object - instance of $class or null if not found - */ - public function findObjectByTranslatedField($field, $value, $class) - { - $entity = null; - $meta = $this->_em->getClassMetadata($class); - $translationMeta = $this->getClassMetadata(); // table inheritance support - if ($meta->hasField($field)) { - $dql = "SELECT trans.foreignKey FROM {$translationMeta->rootEntityName} trans"; - $dql .= ' WHERE trans.objectClass = :class'; - $dql .= ' AND trans.field = :field'; - $dql .= ' AND trans.content = :value'; - $q = $this->_em->createQuery($dql); - $q->setParameters(compact('class', 'field', 'value')); - $q->setMaxResults(1); - $result = $q->getArrayResult(); - $id = count($result) ? $result[0]['foreignKey'] : null; - - if ($id) { - $entity = $this->_em->find($class, $id); - } - } - - return $entity; - } - - /** - * Loads all translations with all translatable - * fields by a given entity primary key - * - * @param mixed $id - primary key value of an entity - * - * @return array - */ - public function findTranslationsByObjectId($id) - { - $result = array(); - if ($id) { - $translationMeta = $this->getClassMetadata(); // table inheritance support - $qb = $this->_em->createQueryBuilder(); - $qb->select('trans.content, trans.field, trans.locale') - ->from($translationMeta->rootEntityName, 'trans') - ->where('trans.foreignKey = :entityId') - ->orderBy('trans.locale'); - $q = $qb->getQuery(); - $data = $q->execute( - array('entityId' => $id), - Query::HYDRATE_ARRAY - ); - - if ($data && is_array($data) && count($data)) { - foreach ($data as $row) { - $result[$row['locale']][$row['field']] = $row['content']; - } - } - } - - return $result; - } - - /** - * Get the currently used TranslatableListener - * - * @throws \Gedmo\Exception\RuntimeException - if listener is not found - * - * @return TranslatableListener - */ - private function getTranslatableListener() - { - if (!$this->listener) { - foreach ($this->_em->getEventManager()->getListeners() as $event => $listeners) { - foreach ($listeners as $hash => $listener) { - if ($listener instanceof TranslatableListener) { - return $this->listener = $listener; - } - } - } - - throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found'); - } - - return $this->listener; - } -} diff --git a/lib/Gedmo/Translatable/Entity/Translation.php b/lib/Gedmo/Translatable/Entity/Translation.php deleted file mode 100644 index 68ba447604..0000000000 --- a/lib/Gedmo/Translatable/Entity/Translation.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @author Miha Vrhovnik - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Xml extends BaseXml -{ - /** - * {@inheritDoc} - */ - public function readExtendedMetadata($meta, array &$config) - { - /** - * @var \SimpleXmlElement $xml - */ - $xml = $this->_getMapping($meta->name); - $xmlDoctrine = $xml; - - $xml = $xml->children(self::GEDMO_NAMESPACE_URI); - - if (($xmlDoctrine->getName() == 'entity' || $xmlDoctrine->getName() == 'mapped-superclass')) { - if (isset($xml->translation)) { - /** - * @var \SimpleXmlElement $data - */ - $data = $xml->translation; - if ($this->_isAttributeSet($data, 'locale')) { - $config['locale'] = $this->_getAttribute($data, 'locale'); - } elseif ($this->_isAttributeSet($data, 'language')) { - $config['locale'] = $this->_getAttribute($data, 'language'); - } - if ($this->_isAttributeSet($data, 'entity')) { - $entity = $this->_getAttribute($data, 'entity'); - if (!$cl = $this->getRelatedClassName($meta, $entity)) { - throw new InvalidMappingException("Translation entity class: {$entity} does not exist."); - } - $config['translationClass'] = $cl; - } - } - } - - if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) { - foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) { - $xmlEmbbededClass = $this->_getMapping($embeddedClassInfo['class']); - $this->inspectElementsForTranslatableFields($xmlEmbbededClass, $config, $propertyName); - } - } - - $this->inspectElementsForTranslatableFields($xmlDoctrine, $config); - - if (!$meta->isMappedSuperclass && $config) { - if (is_array($meta->identifier) && count($meta->identifier) > 1) { - throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->name}"); - } - } - } - - private function inspectElementsForTranslatableFields(\SimpleXMLElement $xml, array &$config, $prefix = null) - { - if (!isset($xml->field)) { - return; - } - - foreach ($xml->field as $mapping) { - $mappingDoctrine = $mapping; - /** - * @var \SimpleXmlElement $mapping - */ - $mapping = $mapping->children(self::GEDMO_NAMESPACE_URI); - $field = null !== $prefix ? $prefix . '.' . $this->_getAttribute($mappingDoctrine, 'name') : $this->_getAttribute($mappingDoctrine, 'name'); - if (isset($mapping->translatable)) { - $config['fields'][] = $field; - /** @var \SimpleXmlElement $data */ - $data = $mapping->translatable; - if ($this->_isAttributeSet($data, 'fallback')) { - $config['fallback'][$field] = 'true' == $this->_getAttribute($data, 'fallback') ? true : false; - } - } - } - } -} diff --git a/lib/Gedmo/Translatable/Mapping/Event/TranslatableAdapter.php b/lib/Gedmo/Translatable/Mapping/Event/TranslatableAdapter.php deleted file mode 100644 index dfd66c943f..0000000000 --- a/lib/Gedmo/Translatable/Mapping/Event/TranslatableAdapter.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface TranslatableAdapter extends AdapterInterface -{ - /** - * Checks if $translationClassName is a subclass - * of personal translation - * - * @param string $translationClassName - * - * @return boolean - */ - public function usesPersonalTranslation($translationClassName); - - /** - * Get default LogEntry class used to store the logs - * - * @return string - */ - public function getDefaultTranslationClass(); - - /** - * Load the translations for a given object - * - * @param object $object - * @param string $translationClass - * @param string $locale - * @param string $objectClass - * - * @return array - */ - public function loadTranslations($object, $translationClass, $locale, $objectClass); - - /** - * Search for existing translation record - * - * @param AbstractWrapper $wrapped - * @param string $locale - * @param string $field - * @param string $translationClass - * @param string $objectClass - * - * @return mixed - null if nothing is found, Translation otherwise - */ - public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $translationClass, $objectClass); - - /** - * Removes all associated translations for given object - * - * @param AbstractWrapper $wrapped - * @param string $transClass - * @param string $objectClass - */ - public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transClass, $objectClass); - - /** - * Inserts the translation record - * - * @param object $translation - */ - public function insertTranslationRecord($translation); - - /** - * Get the transformed value for translation - * storage - * - * @param object $object - * @param string $field - * @param mixed $value - * - * @return mixed - */ - public function getTranslationValue($object, $field, $value = false); - - /** - * Transform the value from database - * for translation - * - * @param object $object - * @param string $field - * @param mixed $value - */ - public function setTranslationValue($object, $field, $value); -} diff --git a/lib/Gedmo/Translator/Document/Translation.php b/lib/Gedmo/Translator/Document/Translation.php deleted file mode 100644 index b314e83e9c..0000000000 --- a/lib/Gedmo/Translator/Document/Translation.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - * - * @MappedSuperclass - */ -abstract class Translation extends BaseTranslation -{ - /** - * @Id - */ - protected $id; - - /** - * @var string $locale - * - * @MongoString - */ - protected $locale; - - /** - * @var string $property - * - * @MongoString - */ - protected $property; - - /** - * @var string $value - * - * @MongoString - */ - protected $value; - - /** - * Get id - * - * @return integer $id - */ - public function getId() - { - return $this->id; - } -} diff --git a/lib/Gedmo/Translator/Entity/Translation.php b/lib/Gedmo/Translator/Entity/Translation.php deleted file mode 100644 index 0b5ba9bf83..0000000000 --- a/lib/Gedmo/Translator/Entity/Translation.php +++ /dev/null @@ -1,60 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - * - * @MappedSuperclass - */ -abstract class Translation extends BaseTranslation -{ - /** - * @var integer $id - * - * @Column(type="integer") - * @Id - * @GeneratedValue - */ - protected $id; - - /** - * @var string $locale - * - * @Column(type="string", length=8) - */ - protected $locale; - - /** - * @var string $property - * - * @Column(type="string", length=32) - */ - protected $property; - - /** - * @var string $value - * - * @Column(type="text", nullable=true) - */ - protected $value; - - /** - * Get id - * - * @return integer $id - */ - public function getId() - { - return $this->id; - } -} diff --git a/lib/Gedmo/Translator/TranslationInterface.php b/lib/Gedmo/Translator/TranslationInterface.php deleted file mode 100644 index 7ad261189b..0000000000 --- a/lib/Gedmo/Translator/TranslationInterface.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface TranslationInterface -{ - /** - * Set translatable - * - * @param string $translatable - */ - public function setTranslatable($translatable); - - /** - * Get translatable - * - * @return string - */ - public function getTranslatable(); - - /** - * Set locale - * - * @param string $locale - */ - public function setLocale($locale); - - /** - * Get locale - * - * @return string - */ - public function getLocale(); - - /** - * Set property - * - * @param string $property - */ - public function setProperty($property); - - /** - * Get property - * - * @return string - */ - public function getProperty(); - - /** - * Set value - * - * @param string $value - * - * @return static - */ - public function setValue($value); - - /** - * Get value - * - * @return string - */ - public function getValue(); -} diff --git a/lib/Gedmo/Tree/Entity/Repository/AbstractTreeRepository.php b/lib/Gedmo/Tree/Entity/Repository/AbstractTreeRepository.php deleted file mode 100644 index 83eec07218..0000000000 --- a/lib/Gedmo/Tree/Entity/Repository/AbstractTreeRepository.php +++ /dev/null @@ -1,248 +0,0 @@ -getEventManager()->getListeners() as $listeners) { - foreach ($listeners as $listener) { - if ($listener instanceof TreeListener) { - $treeListener = $listener; - break; - } - } - if ($treeListener) { - break; - } - } - - if (is_null($treeListener)) { - throw new \Gedmo\Exception\InvalidMappingException('Tree listener was not found on your entity manager, it must be hooked into the event manager'); - } - - $this->listener = $treeListener; - if (!$this->validate()) { - throw new \Gedmo\Exception\InvalidMappingException('This repository cannot be used for tree type: '.$treeListener->getStrategy($em, $class->name)->getName()); - } - - $this->repoUtils = new RepositoryUtils($this->_em, $this->getClassMetadata(), $this->listener, $this); - } - - /** - * @return \Doctrine\ORM\QueryBuilder - */ - protected function getQueryBuilder() - { - return $this->getEntityManager()->createQueryBuilder(); - } - - /** - * Sets the RepositoryUtilsInterface instance - * - * @param \Gedmo\Tree\RepositoryUtilsInterface $repoUtils - * - * @return static - */ - public function setRepoUtils(RepositoryUtilsInterface $repoUtils) - { - $this->repoUtils = $repoUtils; - - return $this; - } - - /** - * Returns the RepositoryUtilsInterface instance - * - * @return \Gedmo\Tree\RepositoryUtilsInterface|null - */ - public function getRepoUtils() - { - return $this->repoUtils; - } - - /** - * {@inheritDoc} - */ - public function childCount($node = null, $direct = false) - { - $meta = $this->getClassMetadata(); - - if (is_object($node)) { - if (!($node instanceof $meta->name)) { - throw new InvalidArgumentException("Node is not related to this repository"); - } - - $wrapped = new EntityWrapper($node, $this->_em); - - if (!$wrapped->hasValidIdentifier()) { - throw new InvalidArgumentException("Node is not managed by UnitOfWork"); - } - } - - $qb = $this->getChildrenQueryBuilder($node, $direct); - - // We need to remove the ORDER BY DQL part since some vendors could throw an error - // in count queries - $dqlParts = $qb->getDQLParts(); - - // We need to check first if there's an ORDER BY DQL part, because resetDQLPart doesn't - // check if its internal array has an "orderby" index - if (isset($dqlParts['orderBy'])) { - $qb->resetDQLPart('orderBy'); - } - - $aliases = $qb->getRootAliases(); - $alias = $aliases[0]; - - $qb->select('COUNT('.$alias.')'); - - return (int) $qb->getQuery()->getSingleScalarResult(); - } - - /** - * @see \Gedmo\Tree\RepositoryUtilsInterface::childrenHierarchy - */ - public function childrenHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false) - { - return $this->repoUtils->childrenHierarchy($node, $direct, $options, $includeNode); - } - - /** - * @see \Gedmo\Tree\RepositoryUtilsInterface::buildTree - */ - public function buildTree(array $nodes, array $options = array()) - { - return $this->repoUtils->buildTree($nodes, $options); - } - - /** - * @see \Gedmo\Tree\RepositoryUtilsInterface::buildTreeArray - */ - public function buildTreeArray(array $nodes) - { - return $this->repoUtils->buildTreeArray($nodes); - } - - /** - * @see \Gedmo\Tree\RepositoryUtilsInterface::setChildrenIndex - */ - public function setChildrenIndex($childrenIndex) - { - $this->repoUtils->setChildrenIndex($childrenIndex); - } - - /** - * @see \Gedmo\Tree\RepositoryUtilsInterface::getChildrenIndex - */ - public function getChildrenIndex() - { - return $this->repoUtils->getChildrenIndex(); - } - - /** - * Checks if current repository is right - * for currently used tree strategy - * - * @return bool - */ - abstract protected function validate(); - - /** - * Get all root nodes query builder - * - * @param string - Sort by field - * @param string - Sort direction ("asc" or "desc") - * - * @return \Doctrine\ORM\QueryBuilder - QueryBuilder object - */ - abstract public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc'); - - /** - * Get all root nodes query - * - * @param string - Sort by field - * @param string - Sort direction ("asc" or "desc") - * - * @return \Doctrine\ORM\Query - Query object - */ - abstract public function getRootNodesQuery($sortByField = null, $direction = 'asc'); - - /** - * Returns a QueryBuilder configured to return an array of nodes suitable for buildTree method - * - * @param object $node - Root node - * @param bool $direct - Obtain direct children? - * @param array $options - Options - * @param boolean $includeNode - Include node in results? - * - * @return \Doctrine\ORM\QueryBuilder - QueryBuilder object - */ - abstract public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = array(), $includeNode = false); - - /** - * Returns a Query configured to return an array of nodes suitable for buildTree method - * - * @param object $node - Root node - * @param bool $direct - Obtain direct children? - * @param array $options - Options - * @param boolean $includeNode - Include node in results? - * - * @return \Doctrine\ORM\Query - Query object - */ - abstract public function getNodesHierarchyQuery($node = null, $direct = false, array $options = array(), $includeNode = false); - - /** - * Get list of children followed by given $node. This returns a QueryBuilder object - * - * @param object $node - if null, all tree nodes will be taken - * @param boolean $direct - true to take only direct children - * @param string $sortByField - field name to sort by - * @param string $direction - sort direction : "ASC" or "DESC" - * @param bool $includeNode - Include the root node in results? - * - * @return \Doctrine\ORM\QueryBuilder - QueryBuilder object - */ - abstract public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); - - /** - * Get list of children followed by given $node. This returns a Query - * - * @param object $node - if null, all tree nodes will be taken - * @param boolean $direct - true to take only direct children - * @param string $sortByField - field name to sort by - * @param string $direction - sort direction : "ASC" or "DESC" - * @param bool $includeNode - Include the root node in results? - * - * @return \Doctrine\ORM\Query - Query object - */ - abstract public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); -} diff --git a/lib/Gedmo/Tree/Entity/Repository/ClosureTreeRepository.php b/lib/Gedmo/Tree/Entity/Repository/ClosureTreeRepository.php deleted file mode 100644 index 0d5a7df82d..0000000000 --- a/lib/Gedmo/Tree/Entity/Repository/ClosureTreeRepository.php +++ /dev/null @@ -1,400 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class ClosureTreeRepository extends AbstractTreeRepository -{ - /** Alias for the level value used in the subquery of the getNodesHierarchy method */ - const SUBQUERY_LEVEL = 'level'; - - /** - * {@inheritDoc} - */ - public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') - { - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $qb = $this->getQueryBuilder(); - $qb->select('node') - ->from($config['useObjectClass'], 'node') - ->where('node.'.$config['parent']." IS NULL"); - - if ($sortByField) { - $qb->orderBy('node.'.$sortByField, strtolower($direction) === 'asc' ? 'asc' : 'desc'); - } - - return $qb; - } - - /** - * {@inheritDoc} - */ - public function getRootNodesQuery($sortByField = null, $direction = 'asc') - { - return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); - } - - /** - * {@inheritDoc} - */ - public function getRootNodes($sortByField = null, $direction = 'asc') - { - return $this->getRootNodesQuery($sortByField, $direction)->getResult(); - } - - /** - * Get the Tree path query by given $node - * - * @param object $node - * - * @throws InvalidArgumentException - if input is not valid - * - * @return Query - */ - public function getPathQuery($node) - { - $meta = $this->getClassMetadata(); - if (!$node instanceof $meta->name) { - throw new InvalidArgumentException("Node is not related to this repository"); - } - if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) { - throw new InvalidArgumentException("Node is not managed by UnitOfWork"); - } - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $closureMeta = $this->_em->getClassMetadata($config['closure']); - - $dql = "SELECT c, node FROM {$closureMeta->name} c"; - $dql .= " INNER JOIN c.ancestor node"; - $dql .= " WHERE c.descendant = :node"; - $dql .= " ORDER BY c.depth DESC"; - $q = $this->_em->createQuery($dql); - $q->setParameters(compact('node')); - - return $q; - } - - /** - * Get the Tree path of Nodes by given $node - * - * @param object $node - * - * @return array - list of Nodes in path - */ - public function getPath($node) - { - return array_map(function (AbstractClosure $closure) { - return $closure->getAncestor(); - }, $this->getPathQuery($node)->getResult()); - } - - /** - * @see getChildrenQueryBuilder - */ - public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - - $qb = $this->getQueryBuilder(); - if ($node !== null) { - if ($node instanceof $meta->name) { - if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) { - throw new InvalidArgumentException("Node is not managed by UnitOfWork"); - } - - $where = 'c.ancestor = :node AND '; - - $qb->select('c, node') - ->from($config['closure'], 'c') - ->innerJoin('c.descendant', 'node'); - - if ($direct) { - $where .= 'c.depth = 1'; - } else { - $where .= 'c.descendant <> :node'; - } - - $qb->where($where); - - if ($includeNode) { - $qb->orWhere('c.ancestor = :node AND c.descendant = :node'); - } - } else { - throw new \InvalidArgumentException("Node is not related to this repository"); - } - } else { - $qb->select('node') - ->from($config['useObjectClass'], 'node'); - if ($direct) { - $qb->where('node.'.$config['parent'].' IS NULL'); - } - } - - if ($sortByField) { - if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) { - $qb->orderBy('node.'.$sortByField, $direction); - } else { - throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}"); - } - } - - if ($node) { - $qb->setParameter('node', $node); - } - - return $qb; - } - - /** - * @see getChildrenQuery - */ - public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); - } - - /** - * @see getChildren - */ - public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - $result = $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode)->getResult(); - if ($node) { - $result = array_map(function (AbstractClosure $closure) { - return $closure->getDescendant(); - }, $result); - } - - return $result; - } - - /** - * {@inheritDoc} - */ - public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode); - } - - /** - * {@inheritDoc} - */ - public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode); - } - - /** - * {@inheritDoc} - */ - public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - return $this->children($node, $direct, $sortByField, $direction, $includeNode); - } - - /** - * Removes given $node from the tree and reparents its descendants - * - * @todo may be improved, to issue single query on reparenting - * - * @param object $node - * - * @throws \Gedmo\Exception\InvalidArgumentException - * @throws \Gedmo\Exception\RuntimeException - if something fails in transaction - */ - public function removeFromTree($node) - { - $meta = $this->getClassMetadata(); - if (!$node instanceof $meta->name) { - throw new InvalidArgumentException("Node is not related to this repository"); - } - $wrapped = new EntityWrapper($node, $this->_em); - if (!$wrapped->hasValidIdentifier()) { - throw new InvalidArgumentException("Node is not managed by UnitOfWork"); - } - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $pk = $meta->getSingleIdentifierFieldName(); - $nodeId = $wrapped->getIdentifier(); - $parent = $wrapped->getPropertyValue($config['parent']); - - $dql = "SELECT node FROM {$config['useObjectClass']} node"; - $dql .= " WHERE node.{$config['parent']} = :node"; - $q = $this->_em->createQuery($dql); - $q->setParameters(compact('node')); - $nodesToReparent = $q->getResult(); - // process updates in transaction - $this->_em->getConnection()->beginTransaction(); - try { - foreach ($nodesToReparent as $nodeToReparent) { - $id = $meta->getReflectionProperty($pk)->getValue($nodeToReparent); - $meta->getReflectionProperty($config['parent'])->setValue($nodeToReparent, $parent); - - $dql = "UPDATE {$config['useObjectClass']} node"; - $dql .= " SET node.{$config['parent']} = :parent"; - $dql .= " WHERE node.{$pk} = :id"; - - $q = $this->_em->createQuery($dql); - $q->setParameters(compact('parent', 'id')); - $q->getSingleScalarResult(); - - $this->listener - ->getStrategy($this->_em, $meta->name) - ->updateNode($this->_em, $nodeToReparent, $node); - - $oid = spl_object_hash($nodeToReparent); - $this->_em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['parent'], $parent); - } - - $dql = "DELETE {$config['useObjectClass']} node"; - $dql .= " WHERE node.{$pk} = :nodeId"; - - $q = $this->_em->createQuery($dql); - $q->setParameters(compact('nodeId')); - $q->getSingleScalarResult(); - $this->_em->getConnection()->commit(); - } catch (\Exception $e) { - $this->_em->close(); - $this->_em->getConnection()->rollback(); - throw new \Gedmo\Exception\RuntimeException('Transaction failed: '.$e->getMessage(), null, $e); - } - // remove from identity map - $this->_em->getUnitOfWork()->removeFromIdentityMap($node); - $node = null; - } - - /** - * Process nodes and produce an array with the - * structure of the tree - * - * @param array - Array of nodes - * - * @return array - Array with tree structure - */ - public function buildTreeArray(array $nodes) - { - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $nestedTree = array(); - $idField = $meta->getSingleIdentifierFieldName(); - $hasLevelProp = !empty($config['level']); - $levelProp = $hasLevelProp ? $config['level'] : self::SUBQUERY_LEVEL; - $childrenIndex = $this->repoUtils->getChildrenIndex(); - - if (count($nodes) > 0) { - $firstLevel = $hasLevelProp ? $nodes[0][0]['descendant'][$levelProp] : $nodes[0][$levelProp]; - $l = 1; // 1 is only an initial value. We could have a tree which has a root node with any level (subtrees) - $refs = array(); - - foreach ($nodes as $n) { - $node = $n[0]['descendant']; - $node[$childrenIndex] = array(); - $level = $hasLevelProp ? $node[$levelProp] : $n[$levelProp]; - - if ($l < $level) { - $l = $level; - } - - if ($l == $firstLevel) { - $tmp = &$nestedTree; - } else { - $tmp = &$refs[$n['parent_id']][$childrenIndex]; - } - - $key = count($tmp); - $tmp[$key] = $node; - $refs[$node[$idField]] = &$tmp[$key]; - } - - unset($refs); - } - - return $nestedTree; - } - - /** - * {@inheritdoc} - */ - public function getNodesHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false) - { - return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult(); - } - - /** - * {@inheritdoc} - */ - public function getNodesHierarchyQuery($node = null, $direct = false, array $options = array(), $includeNode = false) - { - return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); - } - - /** - * {@inheritdoc} - */ - public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = array(), $includeNode = false) - { - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $idField = $meta->getSingleIdentifierFieldName(); - $subQuery = ''; - $hasLevelProp = isset($config['level']) && $config['level']; - - if (!$hasLevelProp) { - $subQuery = ', (SELECT MAX(c2.depth) + 1 FROM '.$config['closure']; - $subQuery .= ' c2 WHERE c2.descendant = c.descendant GROUP BY c2.descendant) AS '.self::SUBQUERY_LEVEL; - } - - $q = $this->_em->createQueryBuilder() - ->select('c, node, p.'.$idField.' AS parent_id'.$subQuery) - ->from($config['closure'], 'c') - ->innerJoin('c.descendant', 'node') - ->leftJoin('node.parent', 'p') - ->addOrderBy(($hasLevelProp ? 'node.'.$config['level'] : self::SUBQUERY_LEVEL), 'asc'); - - if ($node !== null) { - $q->where('c.ancestor = :node'); - $q->setParameters(compact('node')); - } else { - $q->groupBy('c.descendant'); - } - - if (!$includeNode) { - $q->andWhere('c.ancestor != c.descendant'); - } - - $defaultOptions = array(); - $options = array_merge($defaultOptions, $options); - - if (isset($options['childSort']) && is_array($options['childSort']) && - isset($options['childSort']['field']) && isset($options['childSort']['dir'])) { - $q->addOrderBy( - 'node.'.$options['childSort']['field'], - strtolower($options['childSort']['dir']) == 'asc' ? 'asc' : 'desc' - ); - } - - return $q; - } - - /** - * {@inheritdoc} - */ - protected function validate() - { - return $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName() === Strategy::CLOSURE; - } -} diff --git a/lib/Gedmo/Tree/Entity/Repository/NestedTreeRepository.php b/lib/Gedmo/Tree/Entity/Repository/NestedTreeRepository.php deleted file mode 100644 index 2ed1fa2acf..0000000000 --- a/lib/Gedmo/Tree/Entity/Repository/NestedTreeRepository.php +++ /dev/null @@ -1,1053 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - * @method persistAsFirstChild($node) - * @method persistAsFirstChildOf($node, $parent) - * @method persistAsLastChild($node) - * @method persistAsLastChildOf($node, $parent) - * @method persistAsNextSibling($node) - * @method persistAsNextSiblingOf($node, $sibling) - * @method persistAsPrevSibling($node) - * @method persistAsPrevSiblingOf($node, $sibling) - */ -class NestedTreeRepository extends AbstractTreeRepository -{ - /** - * {@inheritDoc} - */ - public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') - { - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $qb = $this->getQueryBuilder(); - $qb - ->select('node') - ->from($config['useObjectClass'], 'node') - ->where($qb->expr()->isNull('node.'.$config['parent'])) - ; - - if ($sortByField !== null) { - $qb->orderBy('node.'.$sortByField, strtolower($direction) === 'asc' ? 'asc' : 'desc'); - } else { - $qb->orderBy('node.'.$config['left'], 'ASC'); - } - - return $qb; - } - - /** - * {@inheritDoc} - */ - public function getRootNodesQuery($sortByField = null, $direction = 'asc') - { - return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); - } - - /** - * {@inheritDoc} - */ - public function getRootNodes($sortByField = null, $direction = 'asc') - { - return $this->getRootNodesQuery($sortByField, $direction)->getResult(); - } - - /** - * Allows the following 'virtual' methods: - * - persistAsFirstChild($node) - * - persistAsFirstChildOf($node, $parent) - * - persistAsLastChild($node) - * - persistAsLastChildOf($node, $parent) - * - persistAsNextSibling($node) - * - persistAsNextSiblingOf($node, $sibling) - * - persistAsPrevSibling($node) - * - persistAsPrevSiblingOf($node, $sibling) - * Inherited virtual methods: - * - find* - * - * @see \Doctrine\ORM\EntityRepository - * - * @throws InvalidArgumentException - If arguments are invalid - * @throws \BadMethodCallException - If the method called is an invalid find* or persistAs* method - * or no find* either persistAs* method at all and therefore an invalid method call. - * - * @return mixed - TreeNestedRepository if persistAs* is called - */ - public function __call($method, $args) - { - if (substr($method, 0, 9) === 'persistAs') { - if (!isset($args[0])) { - throw new \Gedmo\Exception\InvalidArgumentException('Node to persist must be available as first argument'); - } - $node = $args[0]; - $wrapped = new EntityWrapper($node, $this->_em); - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $position = substr($method, 9); - if (substr($method, -2) === 'Of') { - if (!isset($args[1])) { - throw new \Gedmo\Exception\InvalidArgumentException('If "Of" is specified you must provide parent or sibling as the second argument'); - } - $parentOrSibling = $args[1]; - if (strstr($method,'Sibling')) { - $wrappedParentOrSibling = new EntityWrapper($parentOrSibling, $this->_em); - $newParent = $wrappedParentOrSibling->getPropertyValue($config['parent']); - if (null === $newParent && isset($config['root'])) { - throw new UnexpectedValueException("Cannot persist sibling for a root node, tree operation is not possible"); - } - $node->sibling = $parentOrSibling; - $parentOrSibling = $newParent; - } - $wrapped->setPropertyValue($config['parent'], $parentOrSibling); - $position = substr($position, 0, -2); - } - $wrapped->setPropertyValue($config['left'], 0); // simulate changeset - $oid = spl_object_hash($node); - $this->listener - ->getStrategy($this->_em, $meta->name) - ->setNodePosition($oid, $position) - ; - - $this->_em->persist($node); - - return $this; - } - - return parent::__call($method, $args); - } - - /** - * Get the Tree path query builder by given $node - * - * @param object $node - * - * @throws InvalidArgumentException - if input is not valid - * - * @return \Doctrine\ORM\QueryBuilder - */ - public function getPathQueryBuilder($node) - { - $meta = $this->getClassMetadata(); - if (!$node instanceof $meta->name) { - throw new InvalidArgumentException("Node is not related to this repository"); - } - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $wrapped = new EntityWrapper($node, $this->_em); - if (!$wrapped->hasValidIdentifier()) { - throw new InvalidArgumentException("Node is not managed by UnitOfWork"); - } - $left = $wrapped->getPropertyValue($config['left']); - $right = $wrapped->getPropertyValue($config['right']); - $qb = $this->getQueryBuilder(); - $qb->select('node') - ->from($config['useObjectClass'], 'node') - ->where($qb->expr()->lte('node.'.$config['left'], $left)) - ->andWhere($qb->expr()->gte('node.'.$config['right'], $right)) - ->orderBy('node.'.$config['left'], 'ASC') - ; - if (isset($config['root'])) { - $rootId = $wrapped->getPropertyValue($config['root']); - $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); - } - - return $qb; - } - - /** - * Get the Tree path query by given $node - * - * @param object $node - * - * @return \Doctrine\ORM\Query - */ - public function getPathQuery($node) - { - return $this->getPathQueryBuilder($node)->getQuery(); - } - - /** - * Get the Tree path of Nodes by given $node - * - * @param object $node - * - * @return array - list of Nodes in path - */ - public function getPath($node) - { - return $this->getPathQuery($node)->getResult(); - } - - /** - * @see getChildrenQueryBuilder - */ - public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - - $qb = $this->getQueryBuilder(); - $qb->select('node') - ->from($config['useObjectClass'], 'node') - ; - if ($node !== null) { - if ($node instanceof $meta->name) { - $wrapped = new EntityWrapper($node, $this->_em); - if (!$wrapped->hasValidIdentifier()) { - throw new InvalidArgumentException("Node is not managed by UnitOfWork"); - } - if ($direct) { - $qb->where($qb->expr()->eq('node.'.$config['parent'], ':pid')); - $qb->setParameter('pid', $wrapped->getIdentifier()); - } else { - $left = $wrapped->getPropertyValue($config['left']); - $right = $wrapped->getPropertyValue($config['right']); - if ($left && $right) { - $qb->where($qb->expr()->lt('node.'.$config['right'], $right)); - $qb->andWhere($qb->expr()->gt('node.'.$config['left'], $left)); - } - } - if (isset($config['root'])) { - $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $wrapped->getPropertyValue($config['root'])); - } - if ($includeNode) { - $idField = $meta->getSingleIdentifierFieldName(); - $qb->where('('.$qb->getDqlPart('where').') OR node.'.$idField.' = :rootNode'); - $qb->setParameter('rootNode', $node); - } - } else { - throw new \InvalidArgumentException("Node is not related to this repository"); - } - } else { - if ($direct) { - $qb->where($qb->expr()->isNull('node.'.$config['parent'])); - } - } - if (!$sortByField) { - $qb->orderBy('node.'.$config['left'], 'ASC'); - } elseif (is_array($sortByField)) { - $fields = ''; - foreach ($sortByField as $field) { - $fields .= 'node.'.$field.','; - } - $fields = rtrim($fields, ','); - $qb->orderBy($fields, $direction); - } else { - if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) { - $qb->orderBy('node.'.$sortByField, $direction); - } else { - throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}"); - } - } - - return $qb; - } - - /** - * @see getChildrenQuery - */ - public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); - } - - /** - * @see getChildren - */ - public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - $q = $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode); - - return $q->getResult(); - } - - /** - * {@inheritDoc} - */ - public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode); - } - - /** - * {@inheritDoc} - */ - public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode); - } - - /** - * {@inheritDoc} - */ - public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) - { - return $this->children($node, $direct, $sortByField, $direction, $includeNode); - } - - /** - * Get tree leafs query builder - * - * @param object $root - root node in case of root tree is required - * @param string $sortByField - field name to sort by - * @param string $direction - sort direction : "ASC" or "DESC" - * - * @throws InvalidArgumentException - if input is not valid - * - * @return \Doctrine\ORM\QueryBuilder - */ - public function getLeafsQueryBuilder($root = null, $sortByField = null, $direction = 'ASC') - { - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - - if (isset($config['root']) && is_null($root)) { - if (is_null($root)) { - throw new InvalidArgumentException("If tree has root, getLeafs method requires any node of this tree"); - } - } - - $qb = $this->getQueryBuilder(); - $qb->select('node') - ->from($config['useObjectClass'], 'node') - ->where($qb->expr()->eq('node.'.$config['right'], '1 + node.'.$config['left'])) - ; - if (isset($config['root'])) { - if ($root instanceof $meta->name) { - $wrapped = new EntityWrapper($root, $this->_em); - $rootId = $wrapped->getPropertyValue($config['root']); - if (!$rootId) { - throw new InvalidArgumentException("Root node must be managed"); - } - $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); - } else { - throw new InvalidArgumentException("Node is not related to this repository"); - } - } - if (!$sortByField) { - if (isset($config['root'])) { - $qb->addOrderBy('node.'.$config['root'], 'ASC'); - } - $qb->addOrderBy('node.'.$config['left'], 'ASC', true); - } else { - if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) { - $qb->orderBy('node.'.$sortByField, $direction); - } else { - throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}"); - } - } - - return $qb; - } - - /** - * Get tree leafs query - * - * @param object $root - root node in case of root tree is required - * @param string $sortByField - field name to sort by - * @param string $direction - sort direction : "ASC" or "DESC" - * - * @return \Doctrine\ORM\Query - */ - public function getLeafsQuery($root = null, $sortByField = null, $direction = 'ASC') - { - return $this->getLeafsQueryBuilder($root, $sortByField, $direction)->getQuery(); - } - - /** - * Get list of leaf nodes of the tree - * - * @param object $root - root node in case of root tree is required - * @param string $sortByField - field name to sort by - * @param string $direction - sort direction : "ASC" or "DESC" - * - * @return array - */ - public function getLeafs($root = null, $sortByField = null, $direction = 'ASC') - { - return $this->getLeafsQuery($root, $sortByField, $direction)->getResult(); - } - - /** - * Get the query builder for next siblings of the given $node - * - * @param object $node - * @param bool $includeSelf - include the node itself - * - * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid - * - * @return \Doctrine\ORM\QueryBuilder - */ - public function getNextSiblingsQueryBuilder($node, $includeSelf = false) - { - $meta = $this->getClassMetadata(); - if (!$node instanceof $meta->name) { - throw new InvalidArgumentException("Node is not related to this repository"); - } - $wrapped = new EntityWrapper($node, $this->_em); - if (!$wrapped->hasValidIdentifier()) { - throw new InvalidArgumentException("Node is not managed by UnitOfWork"); - } - - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $parent = $wrapped->getPropertyValue($config['parent']); - if (isset($config['root']) && !$parent) { - throw new InvalidArgumentException("Cannot get siblings from tree root node"); - } - - $left = $wrapped->getPropertyValue($config['left']); - - $qb = $this->getQueryBuilder(); - $qb->select('node') - ->from($config['useObjectClass'], 'node') - ->where($includeSelf ? - $qb->expr()->gte('node.'.$config['left'], $left) : - $qb->expr()->gt('node.'.$config['left'], $left) - ) - ->orderBy("node.{$config['left']}", 'ASC') - ; - if ($parent) { - $wrappedParent = new EntityWrapper($parent, $this->_em); - $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); - $qb->setParameter('pid', $wrappedParent->getIdentifier()); - } else { - $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); - } - - return $qb; - } - - /** - * Get the query for next siblings of the given $node - * - * @param object $node - * @param bool $includeSelf - include the node itself - * - * @return \Doctrine\ORM\Query - */ - public function getNextSiblingsQuery($node, $includeSelf = false) - { - return $this->getNextSiblingsQueryBuilder($node, $includeSelf)->getQuery(); - } - - /** - * Find the next siblings of the given $node - * - * @param object $node - * @param bool $includeSelf - include the node itself - * - * @return array - */ - public function getNextSiblings($node, $includeSelf = false) - { - return $this->getNextSiblingsQuery($node, $includeSelf)->getResult(); - } - - /** - * Get query builder for previous siblings of the given $node - * - * @param object $node - * @param bool $includeSelf - include the node itself - * - * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid - * - * @return \Doctrine\ORM\QueryBuilder - */ - public function getPrevSiblingsQueryBuilder($node, $includeSelf = false) - { - $meta = $this->getClassMetadata(); - if (!$node instanceof $meta->name) { - throw new InvalidArgumentException("Node is not related to this repository"); - } - $wrapped = new EntityWrapper($node, $this->_em); - if (!$wrapped->hasValidIdentifier()) { - throw new InvalidArgumentException("Node is not managed by UnitOfWork"); - } - - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $parent = $wrapped->getPropertyValue($config['parent']); - if (isset($config['root']) && !$parent) { - throw new InvalidArgumentException("Cannot get siblings from tree root node"); - } - - $left = $wrapped->getPropertyValue($config['left']); - - $qb = $this->getQueryBuilder(); - $qb->select('node') - ->from($config['useObjectClass'], 'node') - ->where($includeSelf ? - $qb->expr()->lte('node.'.$config['left'], $left) : - $qb->expr()->lt('node.'.$config['left'], $left) - ) - ->orderBy("node.{$config['left']}", 'ASC') - ; - if ($parent) { - $wrappedParent = new EntityWrapper($parent, $this->_em); - $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); - $qb->setParameter('pid', $wrappedParent->getIdentifier()); - } else { - $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); - } - - return $qb; - } - - /** - * Get query for previous siblings of the given $node - * - * @param object $node - * @param bool $includeSelf - include the node itself - * - * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid - * - * @return \Doctrine\ORM\Query - */ - public function getPrevSiblingsQuery($node, $includeSelf = false) - { - return $this->getPrevSiblingsQueryBuilder($node, $includeSelf)->getQuery(); - } - - /** - * Find the previous siblings of the given $node - * - * @param object $node - * @param bool $includeSelf - include the node itself - * - * @return array - */ - public function getPrevSiblings($node, $includeSelf = false) - { - return $this->getPrevSiblingsQuery($node, $includeSelf)->getResult(); - } - - /** - * Move the node down in the same level - * - * @param object $node - * @param int|bool $number integer - number of positions to shift - * boolean - if "true" - shift till last position - * - * @throws \RuntimeException - if something fails in transaction - * - * @return boolean - true if shifted - */ - public function moveDown($node, $number = 1) - { - $result = false; - $meta = $this->getClassMetadata(); - if ($node instanceof $meta->name) { - $nextSiblings = $this->getNextSiblings($node); - if ($numSiblings = count($nextSiblings)) { - $result = true; - if ($number === true) { - $number = $numSiblings; - } elseif ($number > $numSiblings) { - $number = $numSiblings; - } - $this->listener - ->getStrategy($this->_em, $meta->name) - ->updateNode($this->_em, $node, $nextSiblings[$number - 1], Nested::NEXT_SIBLING); - } - } else { - throw new InvalidArgumentException("Node is not related to this repository"); - } - - return $result; - } - - /** - * Move the node up in the same level - * - * @param object $node - * @param int|bool $number integer - number of positions to shift - * boolean - true shift till first position - * - * @throws \RuntimeException - if something fails in transaction - * - * @return boolean - true if shifted - */ - public function moveUp($node, $number = 1) - { - $result = false; - $meta = $this->getClassMetadata(); - if ($node instanceof $meta->name) { - $prevSiblings = array_reverse($this->getPrevSiblings($node)); - if ($numSiblings = count($prevSiblings)) { - $result = true; - if ($number === true) { - $number = $numSiblings; - } elseif ($number > $numSiblings) { - $number = $numSiblings; - } - $this->listener - ->getStrategy($this->_em, $meta->name) - ->updateNode($this->_em, $node, $prevSiblings[$number - 1], Nested::PREV_SIBLING); - } - } else { - throw new InvalidArgumentException("Node is not related to this repository"); - } - - return $result; - } - - /** - * UNSAFE: be sure to backup before running this method when necessary - * - * Removes given $node from the tree and reparents its descendants - * - * @param object $node - * - * @throws \RuntimeException - if something fails in transaction - */ - public function removeFromTree($node) - { - $meta = $this->getClassMetadata(); - if ($node instanceof $meta->name) { - $wrapped = new EntityWrapper($node, $this->_em); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $right = $wrapped->getPropertyValue($config['right']); - $left = $wrapped->getPropertyValue($config['left']); - $rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null; - - if ($right == $left + 1) { - $this->removeSingle($wrapped); - $this->listener - ->getStrategy($this->_em, $meta->name) - ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId); - - return; // node was a leaf - } - // process updates in transaction - $this->_em->getConnection()->beginTransaction(); - try { - $parent = $wrapped->getPropertyValue($config['parent']); - $parentId = null; - if ($parent) { - $wrappedParent = new EntityWrapper($parent, $this->_em); - $parentId = $wrappedParent->getIdentifier(); - } - $pk = $meta->getSingleIdentifierFieldName(); - $nodeId = $wrapped->getIdentifier(); - $shift = -1; - - // in case if root node is removed, children become roots - if (isset($config['root']) && !$parent) { - $qb = $this->getQueryBuilder(); - $qb->select('node.'.$pk, 'node.'.$config['left'], 'node.'.$config['right']) - ->from($config['useObjectClass'], 'node'); - - $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); - $qb->setParameter('pid', $nodeId); - $nodes = $qb->getQuery()->getArrayResult(); - - foreach ($nodes as $newRoot) { - $left = $newRoot[$config['left']]; - $right = $newRoot[$config['right']]; - $rootId = $newRoot[$pk]; - $shift = -($left - 1); - - $qb = $this->getQueryBuilder(); - $qb->update($config['useObjectClass'], 'node'); - $qb->set('node.'.$config['root'], ':rid'); - $qb->setParameter('rid', $rootId); - $qb->where($qb->expr()->eq('node.'.$config['root'], ':rpid')); - $qb->setParameter('rpid', $nodeId); - $qb->andWhere($qb->expr()->gte('node.'.$config['left'], $left)); - $qb->andWhere($qb->expr()->lte('node.'.$config['right'], $right)); - $qb->getQuery()->getSingleScalarResult(); - - $qb = $this->getQueryBuilder(); - $qb->update($config['useObjectClass'], 'node'); - $qb->set('node.'.$config['parent'], ':pid'); - $qb->setParameter('pid', $parentId); - $qb->where($qb->expr()->eq('node.'.$config['parent'], ':rpid')); - $qb->setParameter('rpid', $nodeId); - $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); - $qb->getQuery()->getSingleScalarResult(); - - $this->listener - ->getStrategy($this->_em, $meta->name) - ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1); - $this->listener - ->getStrategy($this->_em, $meta->name) - ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId); - } - } else { - $qb = $this->getQueryBuilder(); - $qb->update($config['useObjectClass'], 'node'); - $qb->set('node.'.$config['parent'], ':pid'); - $qb->setParameter('pid', $parentId); - $qb->where($qb->expr()->eq('node.'.$config['parent'], ':rpid')); - $qb->setParameter('rpid', $nodeId); - if (isset($config['root'])) { - $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); - } - $qb->getQuery()->getSingleScalarResult(); - - $this->listener - ->getStrategy($this->_em, $meta->name) - ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1); - - $this->listener - ->getStrategy($this->_em, $meta->name) - ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId); - } - $this->removeSingle($wrapped); - $this->_em->getConnection()->commit(); - } catch (\Exception $e) { - $this->_em->close(); - $this->_em->getConnection()->rollback(); - throw new \Gedmo\Exception\RuntimeException('Transaction failed', null, $e); - } - } else { - throw new InvalidArgumentException("Node is not related to this repository"); - } - } - - /** - * Reorders $node's sibling nodes and child nodes, - * according to the $sortByField and $direction specified - * - * @param object|null $node - node from which to start reordering the tree; null will reorder everything - * @param string $sortByField - field name to sort by - * @param string $direction - sort direction : "ASC" or "DESC" - * @param boolean $verify - true to verify tree first - * - * @return bool|null - */ - public function reorder($node, $sortByField = null, $direction = 'ASC', $verify = true) - { - $meta = $this->getClassMetadata(); - if ($node instanceof $meta->name || $node === null) { - $config = $this->listener->getConfiguration($this->_em, $meta->name); - if ($verify && is_array($this->verify())) { - return false; - } - - $nodes = $this->children($node, true, $sortByField, $direction); - foreach ($nodes as $node) { - $wrapped = new EntityWrapper($node, $this->_em); - $right = $wrapped->getPropertyValue($config['right']); - $left = $wrapped->getPropertyValue($config['left']); - $this->moveDown($node, true); - if ($left != ($right - 1)) { - $this->reorder($node, $sortByField, $direction, false); - } - } - } else { - throw new InvalidArgumentException("Node is not related to this repository"); - } - } - - /** - * Reorders all nodes in the tree according to the $sortByField and $direction specified. - * - * @param string $sortByField - field name to sort by - * @param string $direction - sort direction : "ASC" or "DESC" - * @param boolean $verify - true to verify tree first - */ - public function reorderAll($sortByField = null, $direction = 'ASC', $verify = true) - { - $this->reorder(null, $sortByField, $direction, $verify); - } - - /** - * Verifies that current tree is valid. - * If any error is detected it will return an array - * with a list of errors found on tree - * - * @return array|bool - true on success,error list on failure - */ - public function verify() - { - if (!$this->childCount()) { - return true; // tree is empty - } - - $errors = array(); - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - if (isset($config['root'])) { - $trees = $this->getRootNodes(); - foreach ($trees as $tree) { - $this->verifyTree($errors, $tree); - } - } else { - $this->verifyTree($errors); - } - - return $errors ?: true; - } - - /** - * NOTE: flush your entity manager after - * - * Tries to recover the tree - * - * @return void - */ - public function recover() - { - if ($this->verify() === true) { - return; - } - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - $self = $this; - $em = $this->_em; - - $doRecover = function ($root, &$count) use ($meta, $config, $self, $em, &$doRecover) { - $lft = $count++; - foreach ($self->getChildren($root, true) as $child) { - $doRecover($child, $count); - } - $rgt = $count++; - $meta->getReflectionProperty($config['left'])->setValue($root, $lft); - $meta->getReflectionProperty($config['right'])->setValue($root, $rgt); - $em->persist($root); - }; - - if (isset($config['root'])) { - foreach ($this->getRootNodes() as $root) { - $count = 1; // reset on every root node - $doRecover($root, $count); - } - } else { - $count = 1; - foreach ($this->getChildren(null, true) as $root) { - $doRecover($root, $count); - } - } - } - - /** - * {@inheritDoc} - */ - public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = array(), $includeNode = false) - { - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - - return $this->childrenQueryBuilder( - $node, - $direct, - isset($config['root']) ? array($config['root'], $config['left']) : $config['left'], - 'ASC', - $includeNode - ); - } - - /** - * {@inheritDoc} - */ - public function getNodesHierarchyQuery($node = null, $direct = false, array $options = array(), $includeNode = false) - { - return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); - } - - /** - * {@inheritdoc} - */ - public function getNodesHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false) - { - return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult(); - } - - /** - * {@inheritdoc} - */ - protected function validate() - { - return $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName() === Strategy::NESTED; - } - - /** - * Collect errors on given tree if - * where are any - * - * @param array $errors - * @param object $root - */ - private function verifyTree(&$errors, $root = null) - { - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - - $identifier = $meta->getSingleIdentifierFieldName(); - $rootId = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($root) : null; - $qb = $this->getQueryBuilder(); - $qb->select($qb->expr()->min('node.'.$config['left'])) - ->from($config['useObjectClass'], 'node') - ; - if (isset($config['root'])) { - $qb->where($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); - } - $min = intval($qb->getQuery()->getSingleScalarResult()); - $edge = $this->listener->getStrategy($this->_em, $meta->name)->max($this->_em, $config['useObjectClass'], $rootId); - // check duplicate right and left values - for ($i = $min; $i <= $edge; $i++) { - $qb = $this->getQueryBuilder(); - $qb->select($qb->expr()->count('node.'.$identifier)) - ->from($config['useObjectClass'], 'node') - ->where($qb->expr()->orX( - $qb->expr()->eq('node.'.$config['left'], $i), - $qb->expr()->eq('node.'.$config['right'], $i) - )) - ; - if (isset($config['root'])) { - $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); - } - $count = intval($qb->getQuery()->getSingleScalarResult()); - if ($count !== 1) { - if ($count === 0) { - $errors[] = "index [{$i}], missing".($root ? ' on tree root: '.$rootId : ''); - } else { - $errors[] = "index [{$i}], duplicate".($root ? ' on tree root: '.$rootId : ''); - } - } - } - // check for missing parents - $qb = $this->getQueryBuilder(); - $qb->select('node') - ->from($config['useObjectClass'], 'node') - ->leftJoin('node.'.$config['parent'], 'parent') - ->where($qb->expr()->isNotNull('node.'.$config['parent'])) - ->andWhere($qb->expr()->isNull('parent.'.$identifier)) - ; - if (isset($config['root'])) { - $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); - } - $nodes = $qb->getQuery()->getArrayResult(); - if (count($nodes)) { - foreach ($nodes as $node) { - $errors[] = "node [{$node[$identifier]}] has missing parent".($root ? ' on tree root: '.$rootId : ''); - } - - return; // loading broken relation can cause infinite loop - } - - $qb = $this->getQueryBuilder(); - $qb->select('node') - ->from($config['useObjectClass'], 'node') - ->where($qb->expr()->lt('node.'.$config['right'], 'node.'.$config['left'])) - ; - if (isset($config['root'])) { - $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); - } - $result = $qb->getQuery() - ->setMaxResults(1) - ->getResult(Query::HYDRATE_ARRAY); - $node = count($result) ? array_shift($result) : null; - - if ($node) { - $id = $node[$identifier]; - $errors[] = "node [{$id}], left is greater than right".($root ? ' on tree root: '.$rootId : ''); - } - - $qb = $this->getQueryBuilder(); - $qb->select('node') - ->from($config['useObjectClass'], 'node') - ; - if (isset($config['root'])) { - $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); - } - $nodes = $qb->getQuery()->getResult(Query::HYDRATE_OBJECT); - - foreach ($nodes as $node) { - $right = $meta->getReflectionProperty($config['right'])->getValue($node); - $left = $meta->getReflectionProperty($config['left'])->getValue($node); - $id = $meta->getReflectionProperty($identifier)->getValue($node); - $parent = $meta->getReflectionProperty($config['parent'])->getValue($node); - if (!$right || !$left) { - $errors[] = "node [{$id}] has invalid left or right values"; - } elseif ($right == $left) { - $errors[] = "node [{$id}] has identical left and right values"; - } elseif ($parent) { - if ($parent instanceof Proxy && !$parent->__isInitialized__) { - $this->_em->refresh($parent); - } - $parentRight = $meta->getReflectionProperty($config['right'])->getValue($parent); - $parentLeft = $meta->getReflectionProperty($config['left'])->getValue($parent); - $parentId = $meta->getReflectionProperty($identifier)->getValue($parent); - if ($left < $parentLeft) { - $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value"; - } elseif ($right > $parentRight) { - $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value"; - } - } else { - $qb = $this->getQueryBuilder(); - $qb->select($qb->expr()->count('node.'.$identifier)) - ->from($config['useObjectClass'], 'node') - ->where($qb->expr()->lt('node.'.$config['left'], $left)) - ->andWhere($qb->expr()->gt('node.'.$config['right'], $right)) - ; - if (isset($config['root'])) { - $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); - } - if ($count = intval($qb->getQuery()->getSingleScalarResult())) { - $errors[] = "node [{$id}] parent field is blank, but it has a parent"; - } - } - } - } - - /** - * Removes single node without touching children - * - * @internal - * - * @param EntityWrapper $wrapped - */ - private function removeSingle(EntityWrapper $wrapped) - { - $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); - - $pk = $meta->getSingleIdentifierFieldName(); - $nodeId = $wrapped->getIdentifier(); - // prevent from deleting whole branch - $qb = $this->getQueryBuilder(); - $qb->update($config['useObjectClass'], 'node') - ->set('node.'.$config['left'], 0) - ->set('node.'.$config['right'], 0); - - $qb->andWhere($qb->expr()->eq('node.'.$pk, ':id')); - $qb->setParameter('id', $nodeId); - $qb->getQuery()->getSingleScalarResult(); - - // remove the node from database - $qb = $this->getQueryBuilder(); - $qb->delete($config['useObjectClass'], 'node'); - $qb->andWhere($qb->expr()->eq('node.'.$pk, ':id')); - $qb->setParameter('id', $nodeId); - $qb->getQuery()->getSingleScalarResult(); - - // remove from identity map - $this->_em->getUnitOfWork()->removeFromIdentityMap($wrapped->getObject()); - } -} diff --git a/lib/Gedmo/Tree/Mapping/Event/TreeAdapter.php b/lib/Gedmo/Tree/Mapping/Event/TreeAdapter.php deleted file mode 100644 index c3e4e98390..0000000000 --- a/lib/Gedmo/Tree/Mapping/Event/TreeAdapter.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface TreeAdapter extends AdapterInterface -{ -} diff --git a/lib/Gedmo/Tree/Node.php b/lib/Gedmo/Tree/Node.php deleted file mode 100644 index 7c6cb10bc1..0000000000 --- a/lib/Gedmo/Tree/Node.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface Node -{ - // use now annotations instead of predefined methods, this interface is not necessary - - /** - * @gedmo:TreeLeft - * to mark the field as "tree left" use property annotation @gedmo:TreeLeft - * it will use this field to store tree left value - */ - - /** - * @gedmo:TreeRight - * to mark the field as "tree right" use property annotation @gedmo:TreeRight - * it will use this field to store tree right value - */ - - /** - * @gedmo:TreeParent - * in every tree there should be link to parent. To identify a relation - * as parent relation to child use @Tree:Ancestor annotation on the related property - */ - - /** - * @gedmo:TreeLevel - * level of node. - */ -} diff --git a/lib/Gedmo/Tree/RepositoryInterface.php b/lib/Gedmo/Tree/RepositoryInterface.php deleted file mode 100644 index 60c13da1a8..0000000000 --- a/lib/Gedmo/Tree/RepositoryInterface.php +++ /dev/null @@ -1,60 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface RepositoryInterface extends RepositoryUtilsInterface -{ - /** - * Get all root nodes - * - * @param string $sortByField - * @param string $direction - * - * @return array - */ - public function getRootNodes($sortByField = null, $direction = 'asc'); - - /** - * Returns an array of nodes suitable for method buildTree - * - * @param object $node - Root node - * @param bool $direct - Obtain direct children? - * @param array $options - Options - * @param boolean $includeNode - Include node in results? - * - * @return array - Array of nodes - */ - public function getNodesHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false); - - /** - * Get list of children followed by given $node - * - * @param object $node - if null, all tree nodes will be taken - * @param boolean $direct - true to take only direct children - * @param string $sortByField - field name to sort by - * @param string $direction - sort direction : "ASC" or "DESC" - * @param bool $includeNode - Include the root node in results? - * - * @return array - list of given $node children, null on failure - */ - public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); - - /** - * Counts the children of given TreeNode - * - * @param object $node - if null counts all records in tree - * @param boolean $direct - true to count only direct children - * - * @throws \Gedmo\Exception\InvalidArgumentException - if input is not valid - * - * @return integer - */ - public function childCount($node = null, $direct = false); -} diff --git a/lib/Gedmo/Tree/RepositoryUtilsInterface.php b/lib/Gedmo/Tree/RepositoryUtilsInterface.php deleted file mode 100644 index 67de6270f9..0000000000 --- a/lib/Gedmo/Tree/RepositoryUtilsInterface.php +++ /dev/null @@ -1,74 +0,0 @@ -LI tree - * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string - * rootOpen: string || Closure ('
    ') - branch start, closure will be given $children as a parameter - * rootClose: string ('
') - branch close - * childStart: string || Closure ('
  • ') - start of node, closure will be given $node as a parameter - * childClose: string ('
  • ') - close of node - * childSort: array || keys allowed: field: field to sort on, dir: direction. 'asc' or 'desc' - * @param boolean $includeNode - Include node on results? - * - * @return array|string - */ - public function childrenHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false); - - /** - * Retrieves the nested array or the decorated output. - * - * Uses options to handle decorations - * NOTE: nodes should be fetched and hydrated as array - * - * @throws \Gedmo\Exception\InvalidArgumentException - * - * @param array $nodes - list o nodes to build tree - * @param array $options : - * decorate: boolean (false) - retrieves tree as UL->LI tree - * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string - * rootOpen: string || Closure ('
      ') - branch start, closure will be given $children as a parameter - * rootClose: string ('
    ') - branch close - * childStart: string || Closure ('
  • ') - start of node, closure will be given $node as a parameter - * childClose: string ('
  • ') - close of node - * - * @return array|string - */ - public function buildTree(array $nodes, array $options = array()); - - /** - * Process nodes and produce an array with the - * structure of the tree - * - * @param array $nodes - Array of nodes - * - * @return array - Array with tree structure - */ - public function buildTreeArray(array $nodes); - - /** - * Sets the current children index. - * - * @param string $childrenIndex - */ - public function setChildrenIndex($childrenIndex); - - /** - * Gets the current children index. - * - * @return string - */ - public function getChildrenIndex(); -} diff --git a/lib/Gedmo/Tree/Strategy/ORM/Closure.php b/lib/Gedmo/Tree/Strategy/ORM/Closure.php deleted file mode 100644 index 3e2c0c03b8..0000000000 --- a/lib/Gedmo/Tree/Strategy/ORM/Closure.php +++ /dev/null @@ -1,474 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Closure implements Strategy -{ - /** - * TreeListener - * - * @var TreeListener - */ - protected $listener = null; - - /** - * List of pending Nodes, which needs to - * be post processed because of having a parent Node - * which requires some additional calculations - * - * @var array - */ - private $pendingChildNodeInserts = array(); - - /** - * List of nodes which has their parents updated, but using - * new nodes. They have to wait until their parents are inserted - * on DB to make the update - * - * @var array - */ - private $pendingNodeUpdates = array(); - - /** - * List of pending Nodes, which needs their "level" - * field value set - * - * @var array - */ - private $pendingNodesLevelProcess = array(); - - /** - * {@inheritdoc} - */ - public function __construct(TreeListener $listener) - { - $this->listener = $listener; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return Strategy::CLOSURE; - } - - /** - * {@inheritdoc} - */ - public function processMetadataLoad($em, $meta) - { - $config = $this->listener->getConfiguration($em, $meta->name); - $closureMetadata = $em->getClassMetadata($config['closure']); - $cmf = $em->getMetadataFactory(); - - if (!$closureMetadata->hasAssociation('ancestor')) { - // create ancestor mapping - $ancestorMapping = array( - 'fieldName' => 'ancestor', - 'id' => false, - 'joinColumns' => array( - array( - 'name' => 'ancestor', - 'referencedColumnName' => 'id', - 'unique' => false, - 'nullable' => false, - 'onDelete' => 'CASCADE', - 'onUpdate' => null, - 'columnDefinition' => null, - ), - ), - 'inversedBy' => null, - 'targetEntity' => $meta->name, - 'cascade' => null, - 'fetch' => ClassMetadataInfo::FETCH_LAZY, - ); - $closureMetadata->mapManyToOne($ancestorMapping); - if (Version::compare('2.3.0-dev') <= 0) { - $closureMetadata->reflFields['ancestor'] = $cmf - ->getReflectionService() - ->getAccessibleProperty($closureMetadata->name, 'ancestor') - ; - } - } - - if (!$closureMetadata->hasAssociation('descendant')) { - // create descendant mapping - $descendantMapping = array( - 'fieldName' => 'descendant', - 'id' => false, - 'joinColumns' => array( - array( - 'name' => 'descendant', - 'referencedColumnName' => 'id', - 'unique' => false, - 'nullable' => false, - 'onDelete' => 'CASCADE', - 'onUpdate' => null, - 'columnDefinition' => null, - ), - ), - 'inversedBy' => null, - 'targetEntity' => $meta->name, - 'cascade' => null, - 'fetch' => ClassMetadataInfo::FETCH_LAZY, - ); - $closureMetadata->mapManyToOne($descendantMapping); - if (Version::compare('2.3.0-dev') <= 0) { - $closureMetadata->reflFields['descendant'] = $cmf - ->getReflectionService() - ->getAccessibleProperty($closureMetadata->name, 'descendant') - ; - } - } - // create unique index on ancestor and descendant - $indexName = substr(strtoupper("IDX_".md5($closureMetadata->name)), 0, 20); - $closureMetadata->table['uniqueConstraints'][$indexName] = array( - 'columns' => array( - $this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor')), - $this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('descendant')), - ), - ); - // this one may not be very useful - $indexName = substr(strtoupper("IDX_".md5($meta->name.'depth')), 0, 20); - $closureMetadata->table['indexes'][$indexName] = array( - 'columns' => array('depth'), - ); - if ($cacheDriver = $cmf->getCacheDriver()) { - $cacheDriver->save($closureMetadata->name."\$CLASSMETADATA", $closureMetadata, null); - } - } - - /** - * {@inheritdoc} - */ - public function onFlushEnd($em, AdapterInterface $ea) - { - } - - /** - * {@inheritdoc} - */ - public function processPrePersist($em, $node) - { - $this->pendingChildNodeInserts[spl_object_hash($node)] = $node; - } - - /** - * {@inheritdoc} - */ - public function processPreUpdate($em, $node) - { - } - - /** - * {@inheritdoc} - */ - public function processPreRemove($em, $node) - { - } - - /** - * {@inheritdoc} - */ - public function processScheduledInsertion($em, $node, AdapterInterface $ea) - { - } - - /** - * {@inheritdoc} - */ - public function processScheduledDelete($em, $entity) - { - } - - protected function getJoinColumnFieldName($association) - { - if (count($association['joinColumnFieldNames']) > 1) { - throw new RuntimeException('More association on field '.$association['fieldName']); - } - - return array_shift($association['joinColumnFieldNames']); - } - - /** - * {@inheritdoc} - */ - public function processPostUpdate($em, $entity, AdapterInterface $ea) - { - $meta = $em->getClassMetadata(get_class($entity)); - $config = $this->listener->getConfiguration($em, $meta->name); - - // Process TreeLevel field value - if (!empty($config)) { - $this->setLevelFieldOnPendingNodes($em); - } - } - - /** - * {@inheritdoc} - */ - public function processPostRemove($em, $entity, AdapterInterface $ea) - { - } - - /** - * {@inheritdoc} - */ - public function processPostPersist($em, $entity, AdapterInterface $ea) - { - $uow = $em->getUnitOfWork(); - - while ($node = array_shift($this->pendingChildNodeInserts)) { - $meta = $em->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($em, $meta->name); - - $identifier = $meta->getSingleIdentifierFieldName(); - $nodeId = $meta->getReflectionProperty($identifier)->getValue($node); - $parent = $meta->getReflectionProperty($config['parent'])->getValue($node); - - $closureClass = $config['closure']; - $closureMeta = $em->getClassMetadata($closureClass); - $closureTable = $closureMeta->getTableName(); - - $ancestorColumnName = $this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor')); - $descendantColumnName = $this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('descendant')); - $depthColumnName = $em->getClassMetadata($config['closure'])->getColumnName('depth'); - - $referenceMapping = $em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor'); - $referenceId = $referenceMapping['sourceToTargetKeyColumns'][$ancestorColumnName]; - - $entries = array( - array( - $ancestorColumnName => $nodeId, - $descendantColumnName => $nodeId, - $depthColumnName => 0, - ), - ); - - if ($parent) { - $dql = "SELECT c, a FROM {$closureMeta->name} c"; - $dql .= " JOIN c.ancestor a"; - $dql .= " WHERE c.descendant = :parent"; - $q = $em->createQuery($dql); - $q->setParameters(compact('parent')); - $ancestors = $q->getArrayResult(); - - foreach ($ancestors as $ancestor) { - $entries[] = array( - $ancestorColumnName => $ancestor['ancestor'][$referenceId], - $descendantColumnName => $nodeId, - $depthColumnName => $ancestor['depth'] + 1, - ); - } - - if (isset($config['level'])) { - $this->pendingNodesLevelProcess[$nodeId] = $node; - } - } elseif (isset($config['level'])) { - $uow->scheduleExtraUpdate($node, array($config['level'] => array(null, 1))); - $ea->setOriginalObjectProperty($uow, spl_object_hash($node), $config['level'], 1); - $levelProp = $meta->getReflectionProperty($config['level']); - $levelProp->setValue($node, 1); - } - - foreach ($entries as $closure) { - if (!$em->getConnection()->insert($closureTable, $closure)) { - throw new RuntimeException('Failed to insert new Closure record'); - } - } - } - - // Process pending node updates - if (!empty($this->pendingNodeUpdates)) { - foreach ($this->pendingNodeUpdates as $info) { - $this->updateNode($em, $info['node'], $info['oldParent']); - } - - $this->pendingNodeUpdates = array(); - } - - // Process TreeLevel field value - $this->setLevelFieldOnPendingNodes($em); - } - - /** - * Process pending entities to set their "level" value - * - * @param \Doctrine\Common\Persistence\ObjectManager $em - */ - protected function setLevelFieldOnPendingNodes(ObjectManager $em) - { - if (!empty($this->pendingNodesLevelProcess)) { - $first = array_slice($this->pendingNodesLevelProcess, 0, 1); - $first = array_shift($first); - $meta = $em->getClassMetadata(get_class($first)); - unset($first); - $identifier = $meta->getIdentifier(); - $mapping = $meta->getFieldMapping($identifier[0]); - $config = $this->listener->getConfiguration($em, $meta->name); - $closureClass = $config['closure']; - $closureMeta = $em->getClassMetadata($closureClass); - $uow = $em->getUnitOfWork(); - - foreach ($this->pendingNodesLevelProcess as $node) { - $children = $em->getRepository($meta->name)->children($node); - - foreach ($children as $child) { - $this->pendingNodesLevelProcess[AbstractWrapper::wrap($child, $em)->getIdentifier()] = $child; - } - } - - // Avoid type conversion performance penalty - $type = 'integer' === $mapping['type'] ? Connection::PARAM_INT_ARRAY : Connection::PARAM_STR_ARRAY; - - // We calculate levels for all nodes - $sql = 'SELECT c.descendant, MAX(c.depth) + 1 AS levelNum '; - $sql .= 'FROM '.$closureMeta->getTableName().' c '; - $sql .= 'WHERE c.descendant IN (?) '; - $sql .= 'GROUP BY c.descendant'; - - $levelsAssoc = $em->getConnection()->executeQuery($sql, array(array_keys($this->pendingNodesLevelProcess)), array($type))->fetchAll(\PDO::FETCH_NUM); - - //create key pair array with resultset - $levels = array(); - foreach( $levelsAssoc as $level ) - { - $levels[$level[0]] = $level[1]; - } - $levelsAssoc = null; - - // Now we update levels - foreach ($this->pendingNodesLevelProcess as $nodeId => $node) { - // Update new level - $level = $levels[$nodeId]; - $levelProp = $meta->getReflectionProperty($config['level']); - $uow->scheduleExtraUpdate( - $node, - array($config['level'] => array( - $levelProp->getValue($node), $level, - )) - ); - $levelProp->setValue($node, $level); - $uow->setOriginalEntityProperty(spl_object_hash($node), $config['level'], $level); - } - - $this->pendingNodesLevelProcess = array(); - } - } - - /** - * {@inheritdoc} - */ - public function processScheduledUpdate($em, $node, AdapterInterface $ea) - { - $meta = $em->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($em, $meta->name); - $uow = $em->getUnitOfWork(); - $changeSet = $uow->getEntityChangeSet($node); - - if (array_key_exists($config['parent'], $changeSet)) { - // If new parent is new, we need to delay the update of the node - // until it is inserted on DB - $parent = $changeSet[$config['parent']][1] ? AbstractWrapper::wrap($changeSet[$config['parent']][1], $em) : null; - - if ($parent && !$parent->getIdentifier()) { - $this->pendingNodeUpdates[spl_object_hash($node)] = array( - 'node' => $node, - 'oldParent' => $changeSet[$config['parent']][0], - ); - } else { - $this->updateNode($em, $node, $changeSet[$config['parent']][0]); - } - } - } - - /** - * Update node and closures - * - * @param EntityManager $em - * @param object $node - * @param object $oldParent - */ - public function updateNode(EntityManager $em, $node, $oldParent) - { - $wrapped = AbstractWrapper::wrap($node, $em); - $meta = $wrapped->getMetadata(); - $config = $this->listener->getConfiguration($em, $meta->name); - $closureMeta = $em->getClassMetadata($config['closure']); - - $nodeId = $wrapped->getIdentifier(); - $parent = $wrapped->getPropertyValue($config['parent']); - $table = $closureMeta->getTableName(); - $conn = $em->getConnection(); - // ensure integrity - if ($parent) { - $dql = "SELECT COUNT(c) FROM {$closureMeta->name} c"; - $dql .= " WHERE c.ancestor = :node"; - $dql .= " AND c.descendant = :parent"; - $q = $em->createQuery($dql); - $q->setParameters(compact('node', 'parent')); - if ($q->getSingleScalarResult()) { - throw new \Gedmo\Exception\UnexpectedValueException("Cannot set child as parent to node: {$nodeId}"); - } - } - - if ($oldParent) { - $subQuery = "SELECT c2.id FROM {$table} c1"; - $subQuery .= " JOIN {$table} c2 ON c1.descendant = c2.descendant"; - $subQuery .= " WHERE c1.ancestor = :nodeId AND c2.depth > c1.depth"; - - $ids = $conn->fetchAll($subQuery, compact('nodeId')); - if ($ids) { - $ids = array_map(function ($el) { - return $el['id']; - }, $ids); - } - // using subquery directly, sqlite acts unfriendly - $query = "DELETE FROM {$table} WHERE id IN (".implode(', ', $ids).")"; - if (!$conn->executeQuery($query)) { - throw new RuntimeException('Failed to remove old closures'); - } - } - - if ($parent) { - $wrappedParent = AbstractWrapper::wrap($parent, $em); - $parentId = $wrappedParent->getIdentifier(); - $query = "SELECT c1.ancestor, c2.descendant, (c1.depth + c2.depth + 1) AS depth"; - $query .= " FROM {$table} c1, {$table} c2"; - $query .= " WHERE c1.descendant = :parentId"; - $query .= " AND c2.ancestor = :nodeId"; - - $closures = $conn->fetchAll($query, compact('nodeId', 'parentId')); - - foreach ($closures as $closure) { - if (!$conn->insert($table, $closure)) { - throw new RuntimeException('Failed to insert new Closure record'); - } - } - } - - if (isset($config['level'])) { - $this->pendingNodesLevelProcess[$nodeId] = $node; - } - } -} diff --git a/lib/Gedmo/Tree/Traits/NestedSet.php b/lib/Gedmo/Tree/Traits/NestedSet.php deleted file mode 100644 index 09950ee2df..0000000000 --- a/lib/Gedmo/Tree/Traits/NestedSet.php +++ /dev/null @@ -1,34 +0,0 @@ -= 5.4 - * - * @author Renaat De Muynck - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -trait NestedSet -{ - - /** - * @var integer - */ - private $root; - - /** - * @var integer - */ - private $level; - - /** - * @var integer - */ - private $left; - - /** - * @var integer - */ - private $right; - -} diff --git a/lib/Gedmo/Tree/Traits/NestedSetEntity.php b/lib/Gedmo/Tree/Traits/NestedSetEntity.php deleted file mode 100644 index df94cd0519..0000000000 --- a/lib/Gedmo/Tree/Traits/NestedSetEntity.php +++ /dev/null @@ -1,43 +0,0 @@ -= 5.4 - * - * @author Renaat De Muynck - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -trait NestedSetEntity -{ - /** - * @var integer - * @Gedmo\TreeRoot - * @ORM\Column(name="root", type="integer", nullable=true) - */ - private $root; - - /** - * @var integer - * @Gedmo\TreeLevel - * @ORM\Column(name="lvl", type="integer") - */ - private $level; - - /** - * @var integer - * @Gedmo\TreeLeft - * @ORM\Column(name="lft", type="integer") - */ - private $left; - - /** - * @var integer - * @Gedmo\TreeRight - * @ORM\Column(name="rgt", type="integer") - */ - private $right; -} diff --git a/lib/Gedmo/Tree/Traits/NestedSetEntityUuid.php b/lib/Gedmo/Tree/Traits/NestedSetEntityUuid.php deleted file mode 100644 index 6d4bed4207..0000000000 --- a/lib/Gedmo/Tree/Traits/NestedSetEntityUuid.php +++ /dev/null @@ -1,25 +0,0 @@ -= 5.4 - * - * @author Benjamin Lazarecki - * - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -trait NestedSetEntityUuid -{ - use NestedSetEntity; - - /** - * @var string - * @Gedmo\TreeRoot - * @ORM\Column(name="root", type="string", nullable=true) - */ - private $root; -} diff --git a/lib/Gedmo/Uploadable/Event/UploadablePostFileProcessEventArgs.php b/lib/Gedmo/Uploadable/Event/UploadablePostFileProcessEventArgs.php deleted file mode 100644 index a75d0fdfbb..0000000000 --- a/lib/Gedmo/Uploadable/Event/UploadablePostFileProcessEventArgs.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ - -class UploadablePostFileProcessEventArgs extends UploadableBaseEventArgs -{ -} diff --git a/lib/Gedmo/Uploadable/Event/UploadablePreFileProcessEventArgs.php b/lib/Gedmo/Uploadable/Event/UploadablePreFileProcessEventArgs.php deleted file mode 100644 index 41a667f352..0000000000 --- a/lib/Gedmo/Uploadable/Event/UploadablePreFileProcessEventArgs.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ - -class UploadablePreFileProcessEventArgs extends UploadableBaseEventArgs -{ -} diff --git a/lib/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorInterface.php b/lib/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorInterface.php deleted file mode 100644 index baf0e560bb..0000000000 --- a/lib/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorInterface.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ - -interface FilenameGeneratorInterface -{ - /** - * Generates a new filename - * - * @param string - Filename without extension - * @param string - Extension with dot: .jpg, .gif, etc - * @param $object - * - * @return string - */ - public static function generate($filename, $extension, $object = null); -} diff --git a/lib/Gedmo/Uploadable/MimeType/MimeTypeGuesserInterface.php b/lib/Gedmo/Uploadable/MimeType/MimeTypeGuesserInterface.php deleted file mode 100644 index 64a37b6e58..0000000000 --- a/lib/Gedmo/Uploadable/MimeType/MimeTypeGuesserInterface.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface MimeTypeGuesserInterface -{ - public function guess($filePath); -} diff --git a/lib/Gedmo/Uploadable/Uploadable.php b/lib/Gedmo/Uploadable/Uploadable.php deleted file mode 100644 index 7abfb89cd5..0000000000 --- a/lib/Gedmo/Uploadable/Uploadable.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -interface Uploadable -{ - // this interface is not necessary to implement - - /** - * @gedmo:Uploadable - * to mark the class as Uploadable use class annotation @gedmo:Uploadable - * this object will be able Uploadable - * example: - * - * @gedmo:Uploadable - * class MyEntity - */ -} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000000..2a8058ceca --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,1292 @@ +parameters: + ignoreErrors: + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 4 + path: src/AbstractTrackingListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/AbstractTrackingListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 3 + path: src/AbstractTrackingListener.php + + - + message: '#^Access to offset ''inherited'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\AssociationFieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/Blameable/Mapping/Driver/Attribute.php + + - + message: '#^Property Gedmo\\Mapping\\Annotation\\Blameable\:\:\$field \(array\\|string\) in isset\(\) is not nullable\.$#' + identifier: isset.property + count: 1 + path: src/Blameable/Mapping/Driver/Attribute.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Blameable/Mapping/Driver/Xml.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Blameable/Mapping/Driver/Yaml.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 2 + path: src/DoctrineExtensions.php + + - + message: '#^Access to offset ''inherited'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\AssociationFieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/IpTraceable/Mapping/Driver/Attribute.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/IpTraceable/Mapping/Driver/Xml.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/IpTraceable/Mapping/Driver/Yaml.php + + - + message: '#^Method Gedmo\\Loggable\\Entity\\Repository\\LogEntryRepository\:\:getLogEntriesQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Loggable/Entity/Repository/LogEntryRepository.php + + - + message: '#^Unable to resolve the template type T in call to method Doctrine\\ORM\\EntityManagerInterface\:\:getReference\(\)$#' + identifier: argument.templateType + count: 1 + path: src/Loggable/Entity/Repository/LogEntryRepository.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\>\:\:newInstance\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Loggable/LoggableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\>\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Loggable/LoggableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 4 + path: src/Loggable/LoggableListener.php + + - + message: '#^Method Gedmo\\Tool\\WrapperInterface\,object,Doctrine\\Persistence\\ObjectManager\>\:\:getIdentifier\(\) invoked with 2 parameters, 0\-1 required\.$#' + identifier: arguments.count + count: 2 + path: src/Loggable/LoggableListener.php + + - + message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:\$associationMappings\.$#' + identifier: property.notFound + count: 1 + path: src/Loggable/Mapping/Driver/Xml.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mapping/Driver/AbstractAnnotationDriver.php + + - + message: '#^Call to an undefined method Doctrine\\Common\\EventArgs\:\:getDocument\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mapping/Event/Adapter/ODM.php + + - + message: '#^Call to an undefined method Doctrine\\Common\\EventArgs\:\:getDocumentManager\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mapping/Event/Adapter/ODM.php + + - + message: '#^Call to an undefined method Doctrine\\Common\\EventArgs\:\:getEntity\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mapping/Event/Adapter/ORM.php + + - + message: '#^Call to an undefined method Doctrine\\Common\\EventArgs\:\:getEntityManager\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mapping/Event/Adapter/ORM.php + + - + message: '#^Method Gedmo\\Mapping\\Event\\Adapter\\ORM\:\:createLifecycleEventArgsInstance\(\) has invalid return type Doctrine\\ORM\\Event\\LifecycleEventArgs\.$#' + identifier: class.notFound + count: 1 + path: src/Mapping/Event/Adapter/ORM.php + + - + message: '#^Access to property \$isMappedSuperclass on an unknown class Doctrine\\ORM\\Mapping\\ClassMetadataInfo\.$#' + identifier: class.notFound + count: 1 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Access to property \$parentClasses on an unknown class Doctrine\\ORM\\Mapping\\ClassMetadataInfo\.$#' + identifier: class.notFound + count: 1 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Access to property \$reflClass on an unknown class Doctrine\\ORM\\Mapping\\ClassMetadataInfo\.$#' + identifier: class.notFound + count: 1 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver\:\:getDefaultDriver\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver\:\:getDrivers\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver\:\:getLocator\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getConfiguration\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Call to method getName\(\) on an unknown class Doctrine\\ORM\\Mapping\\ClassMetadataInfo\.$#' + identifier: class.notFound + count: 4 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Call to method isInheritanceTypeNone\(\) on an unknown class Doctrine\\ORM\\Mapping\\ClassMetadataInfo\.$#' + identifier: class.notFound + count: 1 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Class Doctrine\\ORM\\Mapping\\ClassMetadataInfo not found\.$#' + identifier: class.notFound + count: 1 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Parameter \$meta of method Gedmo\\Mapping\\ExtensionMetadataFactory\:\:getExtensionMetadata\(\) has invalid type Doctrine\\ORM\\Mapping\\ClassMetadataInfo\.$#' + identifier: class.notFound + count: 1 + path: src/Mapping/ExtensionMetadataFactory.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mapping/MappedEventSubscriber.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mapping/MappedEventSubscriber.php + + - + message: '#^Call to method getName\(\) on an unknown class Doctrine\\ORM\\Mapping\\ClassMetadataInfo\.$#' + identifier: class.notFound + count: 1 + path: src/Mapping/MappedEventSubscriber.php + + - + message: '#^Class Doctrine\\ORM\\Mapping\\ClassMetadataInfo not found\.$#' + identifier: class.notFound + count: 1 + path: src/Mapping/MappedEventSubscriber.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/ReferenceIntegrity/ReferenceIntegrityListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 3 + path: src/ReferenceIntegrity/ReferenceIntegrityListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:isCollectionValuedReference\(\)\.$#' + identifier: method.notFound + count: 4 + path: src/ReferenceIntegrity/ReferenceIntegrityListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:isSingleValuedReference\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/ReferenceIntegrity/ReferenceIntegrityListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 4 + path: src/ReferenceIntegrity/ReferenceIntegrityListener.php + + - + message: '#^Access to offset ''inherited'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\AssociationFieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/References/Mapping/Driver/Attribute.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/References/Mapping/Event/Adapter/ODM.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:isInheritanceTypeNone\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/References/Mapping/Event/Adapter/ODM.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getReference\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/References/Mapping/Event/Adapter/ODM.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/References/Mapping/Event/Adapter/ODM.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/References/Mapping/Event/Adapter/ORM.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:isInheritanceTypeNone\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/References/Mapping/Event/Adapter/ORM.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/References/Mapping/Event/Adapter/ORM.php + + - + message: '#^Call to method getClassMetadata\(\) on an unknown class Doctrine\\ODM\\PHPCR\\DocumentManager\.$#' + identifier: class.notFound + count: 2 + path: src/References/Mapping/Event/Adapter/ORM.php + + - + message: '#^Call to method getReference\(\) on an unknown class Doctrine\\ODM\\PHPCR\\DocumentManager\.$#' + identifier: class.notFound + count: 1 + path: src/References/Mapping/Event/Adapter/ORM.php + + - + message: '#^Class Doctrine\\ODM\\PHPCR\\DocumentManager not found\.$#' + identifier: class.notFound + count: 2 + path: src/References/Mapping/Event/Adapter/ORM.php + + - + message: '#^PHPDoc tag @phpstan\-assert for \$dm contains unknown class Doctrine\\ODM\\PHPCR\\DocumentManager\.$#' + identifier: class.notFound + count: 1 + path: src/References/Mapping/Event/Adapter/ORM.php + + - + message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:\$reflClass\.$#' + identifier: property.notFound + count: 4 + path: src/References/ReferencesListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/References/ReferencesListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/References/ReferencesListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sluggable/Handler/InversedRelativeSlugHandler.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sluggable/Handler/InversedRelativeSlugHandler.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Sluggable/Handler/InversedRelativeSlugHandler.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Sluggable/Handler/RelativeSlugHandler.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sluggable/Handler/TreeSlugHandler.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sluggable/Handler/TreeSlugHandler.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 3 + path: src/Sluggable/Handler/TreeSlugHandler.php + + - + message: '#^Access to offset ''inherited'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\AssociationFieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/Sluggable/Mapping/Driver/Attribute.php + + - + message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:\$isMappedSuperclass\.$#' + identifier: property.notFound + count: 1 + path: src/Sluggable/Mapping/Driver/Xml.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sluggable/Mapping/Driver/Xml.php + + - + message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:\$isMappedSuperclass\.$#' + identifier: property.notFound + count: 1 + path: src/Sluggable/Mapping/Driver/Yaml.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sluggable/Mapping/Driver/Yaml.php + + - + message: '#^Call to method getFieldName\(\) on an unknown class Doctrine\\ORM\\Mapping\\ClassMetadataInfo\.$#' + identifier: class.notFound + count: 1 + path: src/Sluggable/Mapping/Event/Adapter/ORM.php + + - + message: '#^Class Doctrine\\ORM\\Mapping\\ClassMetadataInfo not found\.$#' + identifier: class.notFound + count: 1 + path: src/Sluggable/Mapping/Event/Adapter/ORM.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Sluggable/SluggableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 5 + path: src/Sluggable/SluggableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Sluggable/SluggableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Sluggable/SluggableListener.php + + - + message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:\$isMappedSuperclass\.$#' + identifier: property.notFound + count: 1 + path: src/SoftDeleteable/Mapping/Validator.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/SoftDeleteable/Mapping/Validator.php + + - + message: '#^Access to an undefined property Gedmo\\SoftDeleteable\\Query\\TreeWalker\\Exec\\MultiTableDeleteExecutor\:\:\$_sqlStatements\.$#' + identifier: property.notFound + count: 1 + path: src/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php + + - + message: '#^Call to function property_exists\(\) with \$this\(Gedmo\\SoftDeleteable\\Query\\TreeWalker\\Exec\\MultiTableDeleteExecutor\) and ''sqlStatements'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: src/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php + + - + message: '#^Method Gedmo\\SoftDeleteable\\Query\\TreeWalker\\SoftDeleteableWalker\:\:__construct\(\) has parameter \$parserResult with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php + + - + message: '#^Method Gedmo\\SoftDeleteable\\Query\\TreeWalker\\SoftDeleteableWalker\:\:__construct\(\) has parameter \$query with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php + + - + message: '#^Method Gedmo\\Sortable\\Entity\\Repository\\SortableRepository\:\:getBySortableGroupsQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Sortable/Entity/Repository/SortableRepository.php + + - + message: '#^Access to offset ''inherited'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\AssociationFieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/Sortable/Mapping/Driver/Attribute.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sortable/Mapping/Driver/Xml.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sortable/Mapping/Driver/Yaml.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 8 + path: src/Sortable/SortableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sortable/SortableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 4 + path: src/Sortable/SortableListener.php + + - + message: '#^Call to an undefined method Gedmo\\Sortable\\Mapping\\Event\\SortableAdapter\:\:getMaxPosition\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sortable/SortableListener.php + + - + message: '#^Call to an undefined method Gedmo\\Sortable\\Mapping\\Event\\SortableAdapter\:\:updatePositions\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Sortable/SortableListener.php + + - + message: '#^Access to offset ''inherited'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\AssociationFieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/Timestampable/Mapping/Driver/Attribute.php + + - + message: '#^Property Gedmo\\Mapping\\Annotation\\Timestampable\:\:\$field \(array\\|string\) in isset\(\) is not nullable\.$#' + identifier: isset.property + count: 1 + path: src/Timestampable/Mapping/Driver/Attribute.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Timestampable/Mapping/Driver/Xml.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Timestampable/Mapping/Driver/Yaml.php + + - + message: '#^Call to function property_exists\(\) with \$this\(Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator\) and ''_em'' will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: src/Translatable/Hydrator/ORM/ObjectHydrator.php + + - + message: '#^Method Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator\:\:hydrateAllData\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Translatable/Hydrator/ORM/ObjectHydrator.php + + - + message: '#^Call to function property_exists\(\) with \$this\(Gedmo\\Translatable\\Hydrator\\ORM\\SimpleObjectHydrator\) and ''_em'' will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: src/Translatable/Hydrator/ORM/SimpleObjectHydrator.php + + - + message: '#^Method Gedmo\\Translatable\\Hydrator\\ORM\\SimpleObjectHydrator\:\:hydrateAllData\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Translatable/Hydrator/ORM/SimpleObjectHydrator.php + + - + message: '#^Access to offset ''inherited'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\AssociationFieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/Translatable/Mapping/Driver/Attribute.php + + - + message: '#^Access to offset ''association'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\FieldMapping\.$#' + identifier: class.notFound + count: 2 + path: src/Translatable/Mapping/Event/Adapter/ODM.php + + - + message: '#^Access to offset ''fieldName'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\FieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/Translatable/Mapping/Event/Adapter/ODM.php + + - + message: '#^Access to offset ''mappedBy'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\FieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/Translatable/Mapping/Event/Adapter/ODM.php + + - + message: '#^Access to offset ''targetDocument'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\FieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/Translatable/Mapping/Event/Adapter/ODM.php + + - + message: '#^Call to method getAssociationMappings\(\) on an unknown class Doctrine\\ORM\\Mapping\\ClassMetadataInfo\.$#' + identifier: class.notFound + count: 1 + path: src/Translatable/Mapping/Event/Adapter/ORM.php + + - + message: '#^Class Doctrine\\ORM\\Mapping\\ClassMetadataInfo not found\.$#' + identifier: class.notFound + count: 1 + path: src/Translatable/Mapping/Event/Adapter/ORM.php + + - + message: '#^Access to an undefined property Doctrine\\ORM\\Query\\AST\\Join\:\:\$aliasIdentificationVariable\.$#' + identifier: property.notFound + count: 1 + path: src/Translatable/Query/TreeWalker/TranslationWalker.php + + - + message: '#^Access to an undefined property Doctrine\\ORM\\Query\\AST\\Node\:\:\$aliasIdentificationVariable\.$#' + identifier: property.notFound + count: 1 + path: src/Translatable/Query/TreeWalker/TranslationWalker.php + + - + message: '#^Method Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker\:\:__construct\(\) has parameter \$parserResult with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/Translatable/Query/TreeWalker/TranslationWalker.php + + - + message: '#^Method Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker\:\:__construct\(\) has parameter \$query with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/Translatable/Query/TreeWalker/TranslationWalker.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Translatable/TranslatableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:newInstance\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Translatable/TranslatableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 4 + path: src/Translatable/TranslatableListener.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\AbstractTreeRepository\:\:getChildrenQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/AbstractTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\AbstractTreeRepository\:\:getNodesHierarchyQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/AbstractTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\AbstractTreeRepository\:\:getRootNodesQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/AbstractTreeRepository.php + + - + message: '#^Call to an undefined method Gedmo\\Tree\\Strategy\:\:updateNode\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Tree/Entity/Repository/ClosureTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\ClosureTreeRepository\:\:childrenQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/ClosureTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\ClosureTreeRepository\:\:getChildrenQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/ClosureTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\ClosureTreeRepository\:\:getNodesHierarchyQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/ClosureTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\ClosureTreeRepository\:\:getPathQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/ClosureTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\ClosureTreeRepository\:\:getRootNodesQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/ClosureTreeRepository.php + + - + message: '#^Parameter \#1 \$association of method Gedmo\\Tree\\Entity\\Repository\\ClosureTreeRepository\\:\:getJoinColumnFieldName\(\) expects array\, Doctrine\\ORM\\Mapping\\AssociationMapping given\.$#' + identifier: argument.type + count: 2 + path: src/Tree/Entity/Repository/ClosureTreeRepository.php + + - + message: '#^Strict comparison using \!\=\= between array\{\} and non\-empty\-array will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue + count: 1 + path: src/Tree/Entity/Repository/ClosureTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\MaterializedPathRepository\:\:getChildrenQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/MaterializedPathRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\MaterializedPathRepository\:\:getNodesHierarchyQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/MaterializedPathRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\MaterializedPathRepository\:\:getPathQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/MaterializedPathRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\MaterializedPathRepository\:\:getRootNodesQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/MaterializedPathRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\MaterializedPathRepository\:\:getTreeQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/MaterializedPathRepository.php + + - + message: '#^Call to an undefined method Gedmo\\Tree\\Strategy\:\:max\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Call to an undefined method Gedmo\\Tree\\Strategy\:\:setNodePosition\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Call to an undefined method Gedmo\\Tree\\Strategy\:\:shiftRL\(\)\.$#' + identifier: method.notFound + count: 3 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Call to an undefined method Gedmo\\Tree\\Strategy\:\:shiftRangeRL\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Call to an undefined method Gedmo\\Tree\\Strategy\:\:updateNode\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\NestedTreeRepository\:\:childrenQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\NestedTreeRepository\:\:getChildrenQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\NestedTreeRepository\:\:getLeafsQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\NestedTreeRepository\:\:getNextSiblingsQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\NestedTreeRepository\:\:getNodesHierarchyQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\NestedTreeRepository\:\:getPrevSiblingsQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Method Gedmo\\Tree\\Entity\\Repository\\NestedTreeRepository\:\:getRootNodesQuery\(\) return type with generic class Doctrine\\ORM\\Query does not specify its types\: TKey, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Tree/Entity/Repository/NestedTreeRepository.php + + - + message: '#^Call to function property_exists\(\) with \$this\(Gedmo\\Tree\\Hydrator\\ORM\\TreeObjectHydrator\) and ''_em'' will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: src/Tree/Hydrator/ORM/TreeObjectHydrator.php + + - + message: '#^Method Gedmo\\Tree\\Hydrator\\ORM\\TreeObjectHydrator\:\:hydrateAllData\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Tree/Hydrator/ORM/TreeObjectHydrator.php + + - + message: '#^Access to offset ''inherited'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\AssociationFieldMapping\.$#' + identifier: class.notFound + count: 1 + path: src/Tree/Mapping/Driver/Attribute.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 6 + path: src/Tree/Mapping/Validator.php + + - + message: '#^Method Gedmo\\Tree\\RepositoryUtils\:\:buildTreeArray\(\) should return array\\> but returns array\, ArrayAccess\>\.$#' + identifier: return.type + count: 1 + path: src/Tree/RepositoryUtils.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Tree/Strategy/AbstractMaterializedPath.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 8 + path: src/Tree/Strategy/AbstractMaterializedPath.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getIdentifierValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Tree/Strategy/AbstractMaterializedPath.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getSingleIdentifierFieldName\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Tree/Strategy/AbstractMaterializedPath.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 4 + path: src/Tree/Strategy/AbstractMaterializedPath.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getReference\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Tree/Strategy/AbstractMaterializedPath.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Tree/Strategy/AbstractMaterializedPath.php + + - + message: '#^Call to an undefined method Doctrine\\ORM\\EntityRepository\\:\:children\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Tree/Strategy/ORM/Closure.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Tree/TreeListener.php + + - + message: '#^Method Gedmo\\Uploadable\\Event\\UploadableBaseEventArgs\:\:__construct\(\) has parameter \$config with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Uploadable/Event/UploadableBaseEventArgs.php + + - + message: '#^Method Gedmo\\Uploadable\\Event\\UploadableBaseEventArgs\:\:getExtensionConfiguration\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Uploadable/Event/UploadableBaseEventArgs.php + + - + message: '#^Property Gedmo\\Uploadable\\Event\\UploadableBaseEventArgs\:\:\$config is never read, only written\.$#' + identifier: property.onlyWritten + count: 1 + path: src/Uploadable/Event/UploadableBaseEventArgs.php + + - + message: '#^Property Gedmo\\Uploadable\\Event\\UploadableBaseEventArgs\:\:\$config type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Uploadable/Event/UploadableBaseEventArgs.php + + - + message: '#^Property Gedmo\\Uploadable\\Event\\UploadableBaseEventArgs\:\:\$extensionConfiguration is never written, only read\.$#' + identifier: property.onlyRead + count: 1 + path: src/Uploadable/Event/UploadableBaseEventArgs.php + + - + message: '#^Property Gedmo\\Uploadable\\Event\\UploadableBaseEventArgs\:\:\$extensionConfiguration type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Uploadable/Event/UploadableBaseEventArgs.php + + - + message: '#^Method Gedmo\\Uploadable\\FileInfo\\FileInfoArray\:\:getName\(\) never returns null so it can be removed from the return type\.$#' + identifier: return.unusedType + count: 1 + path: src/Uploadable/FileInfo/FileInfoArray.php + + - + message: '#^Method Gedmo\\Uploadable\\FileInfo\\FileInfoArray\:\:getSize\(\) never returns null so it can be removed from the return type\.$#' + identifier: return.unusedType + count: 1 + path: src/Uploadable/FileInfo/FileInfoArray.php + + - + message: '#^Method Gedmo\\Uploadable\\FileInfo\\FileInfoArray\:\:getTmpName\(\) never returns null so it can be removed from the return type\.$#' + identifier: return.unusedType + count: 1 + path: src/Uploadable/FileInfo/FileInfoArray.php + + - + message: '#^Method Gedmo\\Uploadable\\FileInfo\\FileInfoArray\:\:getType\(\) never returns null so it can be removed from the return type\.$#' + identifier: return.unusedType + count: 1 + path: src/Uploadable/FileInfo/FileInfoArray.php + + - + message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:\$isMappedSuperclass\.$#' + identifier: property.notFound + count: 1 + path: src/Uploadable/Mapping/Validator.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldMapping\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Uploadable/Mapping/Validator.php + + - + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse + count: 1 + path: src/Uploadable/MimeType/MimeTypeGuesser.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Uploadable/UploadableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setFieldValue\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Uploadable/UploadableListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 3 + path: src/Uploadable/UploadableListener.php + + - + message: '#^Class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 2 + path: tests/Gedmo/DoctrineExtensionsTest.php + + - + message: '#^Property Gedmo\\Tests\\Mapping\\Fixture\\Attribute\\TranslatableModel\:\:\$title \(string\|null\) is never assigned string so it can be removed from the property type\.$#' + identifier: property.unusedType + count: 1 + path: tests/Gedmo/Mapping/Fixture/Attribute/TranslatableModel.php + + - + message: '#^Property Gedmo\\Tests\\Mapping\\Fixture\\Attribute\\TranslatableModel\:\:\$titleFallbackFalse \(string\|null\) is never assigned string so it can be removed from the property type\.$#' + identifier: property.unusedType + count: 1 + path: tests/Gedmo/Mapping/Fixture/Attribute/TranslatableModel.php + + - + message: '#^Property Gedmo\\Tests\\Mapping\\Fixture\\Attribute\\TranslatableModel\:\:\$titleFallbackTrue \(string\|null\) is never assigned string so it can be removed from the property type\.$#' + identifier: property.unusedType + count: 1 + path: tests/Gedmo/Mapping/Fixture/Attribute/TranslatableModel.php + + - + message: '#^Class Gedmo\\Tests\\Translatable\\Fixture\\CategoryTranslation not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Mapping/Fixture/Category.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 2 + path: tests/Gedmo/Mapping/MappingEventSubscriberTest.php + + - + message: '#^Parameter \#1 \$driverImpl of method Doctrine\\ORM\\Configuration\:\:setMetadataDriverImpl\(\) expects Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver, Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver given\.$#' + identifier: argument.type + count: 2 + path: tests/Gedmo/Mapping/MappingEventSubscriberTest.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Mapping/MappingTest.php + + - + message: '#^Parameter \#1 \$driverImpl of method Doctrine\\ORM\\Configuration\:\:setMetadataDriverImpl\(\) expects Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver, Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver given\.$#' + identifier: argument.type + count: 1 + path: tests/Gedmo/Mapping/MappingTest.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:mapField\(\)\.$#' + identifier: method.notFound + count: 2 + path: tests/Gedmo/Mapping/MetadataFactory/CustomDriverTest.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:setIdGeneratorType\(\)\.$#' + identifier: method.notFound + count: 1 + path: tests/Gedmo/Mapping/MetadataFactory/CustomDriverTest.php + + - + message: '#^Class Doctrine\\ORM\\Id\\IdentityGenerator does not have a constructor and must be instantiated without any parameters\.$#' + identifier: new.noConstructor + count: 1 + path: tests/Gedmo/Mapping/MetadataFactory/ForcedMetadataTest.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Mapping/MetadataFactory/ForcedMetadataTest.php + + - + message: '#^Parameter \#1 \$driverImpl of method Doctrine\\ORM\\Configuration\:\:setMetadataDriverImpl\(\) expects Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver, Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver given\.$#' + identifier: argument.type + count: 1 + path: tests/Gedmo/Mapping/MetadataFactory/ForcedMetadataTest.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getReflectionProperty\(\)\.$#' + identifier: method.notFound + count: 2 + path: tests/Gedmo/Mapping/Mock/Extension/Encoder/EncoderListener.php + + - + message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' + identifier: method.notFound + count: 2 + path: tests/Gedmo/Mapping/Mock/Extension/Encoder/EncoderListener.php + + - + message: '#^Access to offset ''inherited'' on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\AssociationFieldMapping\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Driver/Attribute.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 2 + path: tests/Gedmo/Mapping/MultiManagerMappingTest.php + + - + message: '#^Parameter \#1 \$nestedDriver of method Doctrine\\Persistence\\Mapping\\Driver\\MappingDriverChain\:\:addDriver\(\) expects Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver, Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver given\.$#' + identifier: argument.type + count: 1 + path: tests/Gedmo/Mapping/ORMMappingTestCase.php + + - + message: '#^Parameter \#1 \$nestedDriver of method Doctrine\\Persistence\\Mapping\\Driver\\MappingDriverChain\:\:addDriver\(\) expects Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver, Doctrine\\ORM\\Mapping\\Driver\\YamlDriver given\.$#' + identifier: argument.type + count: 1 + path: tests/Gedmo/Mapping/ORMMappingTestCase.php + + - + message: '#^Property Gedmo\\Tests\\Mapping\\ReferenceIntegrityMappingTest\:\:\$dm is never written, only read\.$#' + identifier: property.onlyRead + count: 1 + path: tests/Gedmo/Mapping/ReferenceIntegrityMappingTest.php + + - + message: '#^Property Gedmo\\Tests\\Mapping\\ReferenceIntegrityMappingTest\:\:\$referenceIntegrity is never written, only read\.$#' + identifier: property.onlyRead + count: 1 + path: tests/Gedmo/Mapping/ReferenceIntegrityMappingTest.php + + - + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable + count: 1 + path: tests/Gedmo/Mapping/ReferenceIntegrityMappingTest.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Mapping/TreeMappingTest.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\YamlDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Mapping/TreeMappingTest.php + + - + message: '#^Parameter \#1 \$nestedDriver of method Doctrine\\Persistence\\Mapping\\Driver\\MappingDriverChain\:\:addDriver\(\) expects Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver, Doctrine\\ORM\\Mapping\\Driver\\YamlDriver given\.$#' + identifier: argument.type + count: 1 + path: tests/Gedmo/Mapping/TreeMappingTest.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Mapping/Xml/ClosureTreeMappingTest.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Mapping/Xml/MaterializedPathTreeMappingTest.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Mapping/Xml/ReferencesMappingTest.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Mapping/Xml/TranslatableMappingTest.php + + - + message: '#^Method Gedmo\\Tests\\Sluggable\\Fixture\\Doctrine\\FakeFilter\:\:addFilterConstraint\(\) has parameter \$targetTableAlias with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/Gedmo/Sluggable/Fixture/Doctrine/FakeFilter.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\YamlDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Sluggable/Issue/Issue116Test.php + + - + message: '#^Parameter \#1 \$nestedDriver of method Doctrine\\Persistence\\Mapping\\Driver\\MappingDriverChain\:\:addDriver\(\) expects Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver, Doctrine\\ORM\\Mapping\\Driver\\YamlDriver given\.$#' + identifier: argument.type + count: 1 + path: tests/Gedmo/Sluggable/Issue/Issue116Test.php + + - + message: '#^Parameter \$args of method Gedmo\\Tests\\SoftDeleteable\\Fixture\\Listener\\WithLifecycleEventArgsFromORMTypeListener\:\:postSoftDelete\(\) has invalid type Doctrine\\ORM\\Event\\LifecycleEventArgs\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/SoftDeleteable/Fixture/Listener/WithLifecycleEventArgsFromORMTypeListener.php + + - + message: '#^Parameter \$args of method Gedmo\\Tests\\SoftDeleteable\\Fixture\\Listener\\WithLifecycleEventArgsFromORMTypeListener\:\:preSoftDelete\(\) has invalid type Doctrine\\ORM\\Event\\LifecycleEventArgs\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/SoftDeleteable/Fixture/Listener/WithLifecycleEventArgsFromORMTypeListener.php + + - + message: '#^Method Gedmo\\Tests\\Timestampable\\Fixture\\ArticleCarbon\:\:getCreated\(\) should return Carbon\\Carbon\|null but returns DateTime\|null\.$#' + identifier: return.type + count: 1 + path: tests/Gedmo/Timestampable/Fixture/ArticleCarbon.php + + - + message: '#^Call to function method_exists\(\) with Doctrine\\ORM\\EntityManager\|null and ''merge'' will always evaluate to false\.$#' + identifier: function.impossibleType + count: 2 + path: tests/Gedmo/Timestampable/TimestampableTest.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Tool/BaseTestCaseOM.php + + - + message: '#^Method Gedmo\\Tests\\Tool\\BaseTestCaseOM\:\:getORMDriver\(\) should return Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver but returns Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\.$#' + identifier: return.type + count: 1 + path: tests/Gedmo/Tool/BaseTestCaseOM.php + + - + message: '#^Instantiated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver not found\.$#' + identifier: class.notFound + count: 1 + path: tests/Gedmo/Tool/BaseTestCaseORM.php + + - + message: '#^Method Gedmo\\Tests\\Tool\\BaseTestCaseORM\:\:getMetadataDriverImplementation\(\) should return Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver but returns Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\.$#' + identifier: return.type + count: 1 + path: tests/Gedmo/Tool/BaseTestCaseORM.php + + - + message: '#^Property Gedmo\\Tests\\Tree\\MaterializedPathODMMongoDBRepositoryTest\:\:\$repo \(Gedmo\\Tree\\Document\\MongoDB\\Repository\\MaterializedPathRepository\\) does not accept Doctrine\\ORM\\EntityRepository\\.$#' + identifier: assign.propertyType + count: 1 + path: tests/Gedmo/Tree/MaterializedPathODMMongoDBRepositoryTest.php + + - + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable + count: 1 + path: tests/Gedmo/Tree/MaterializedPathODMMongoDBTreeLockingTest.php + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000000..5c11c7bc8e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,31 @@ +includes: + - phpstan-baseline.neon + - vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + +parameters: + level: 6 + paths: + - src + - tests + bootstrapFiles: + - tests/bootstrap.php + treatPhpDocTypesAsCertain: false + ignoreErrors: + - + identifier: trait.unused + - '#^Property Gedmo\\Tests\\.+\\Fixture\\[^:]+::\$\w+ is never written, only read\.$#' + - '#^Property Gedmo\\Tests\\.+\\Fixture\\[^:]+::\$\w+ is never read, only written\.$#' + - '#^Property Gedmo\\Tests\\.+\\Fixture\\[^:]+::\$\w+ is unused\.$#' + - '#^Property Gedmo\\Tests\\.+\\Fixture\\[^:]+::\$\w+ (.*) is never assigned (.*) so it can be removed from the property type\.$#' + - '#^Method Gedmo\\(?:[^\\]+\\)*Mapping\\Driver[^:]+::readExtendedMetadata\(\) with return type void returns [\w\|<>\s,]+ but should not return anything\.$#' + - '#^Result of method Gedmo\\Mapping\\Driver::readExtendedMetadata\(\) \(void\) is used\.$#' + excludePaths: + # Deprecated and unused class, interface does not exist as of 4.0 + - src/Tool/Logging/DBAL/QueryAnalyzer.php + # Generates non-ignorable errors like " Parameter #1 $method (string) of method Gedmo\Tree\Entity\Repository\NestedTreeRepository::__call() is not contravariant with parameter #1 $method (mixed) of method Doctrine\ORM\EntityRepository::__call()." + - src/Tool/ORM/Repository/EntityRepositoryCompat.php + # Uses a tracking policy that was removed in ORM 3, PHPStan crashes on this file + - tests/Gedmo/Sortable/Fixture/NotifyNode.php + # Generates non-ignorable errors regarding covariance due to the internal compat layer + - tests/Gedmo/Translatable/Fixture/Type/Custom.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000000..10b665c67b --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,76 @@ + + + + + ./src + + + + + ./tests/Gedmo/Translatable/ + + + ./tests/Gedmo/Sluggable/ + + + ./tests/Gedmo/Sortable/ + + + ./tests/Gedmo/Tree/ + + + ./tests/Gedmo/Timestampable/ + + + ./tests/Gedmo/Blameable/ + + + ./tests/Gedmo/IpTraceable/ + + + ./tests/Gedmo/Mapping/ + + + ./tests/Gedmo/Loggable/ + + + ./tests/Gedmo/Sortable/ + + + ./tests/Gedmo/Wrapper/ + + + ./tests/Gedmo/Translator/ + + + ./tests/Gedmo/SoftDeleteable/ + + + ./tests/Gedmo/Uploadable/ + + + ./tests/Gedmo/ReferenceIntegrity/ + + + ./tests/Gedmo/References/ + + + + + + + + + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000000..ebb3971a6a --- /dev/null +++ b/rector.php @@ -0,0 +1,33 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector; +use Rector\ValueObject\PhpVersion; + +return RectorConfig::configure() + ->withPaths([ + __DIR__.'/src', + __DIR__.'/tests', + __DIR__.'/example', + ]) + ->withPhpVersion(PhpVersion::PHP_74) + ->withPhpSets() + ->withConfiguredRule(TypedPropertyFromAssignsRector::class, []) + ->withSkip([ + TypedPropertyFromAssignsRector::class => [ + __DIR__.'/src/Mapping/MappedEventSubscriber.php', // Rector is trying to set a type on the $annotationReader property which requires a union type, not supported on PHP 7.4 + __DIR__.'/tests/Gedmo/Blameable/Fixture/Entity/Company.php', // @todo: Remove this when fixing the configuration for the `Company::$created` property + __DIR__.'/tests/Gedmo/Wrapper/Fixture/Entity/CompositeRelation.php', // @todo: Remove this when https://github.com/doctrine/orm/issues/8255 is solved + ], + ]) + ->withImportNames(true, true, false) +; diff --git a/schemas/orm/doctrine-extensions-mapping-2-2.xsd b/schemas/orm/doctrine-extensions-mapping-2-2.xsd index 4efec0aac0..71f30049a1 100644 --- a/schemas/orm/doctrine-extensions-mapping-2-2.xsd +++ b/schemas/orm/doctrine-extensions-mapping-2-2.xsd @@ -25,9 +25,11 @@ + + @@ -42,6 +44,7 @@ + @@ -63,6 +66,7 @@ + @@ -104,6 +108,8 @@ + + diff --git a/lib/Gedmo/AbstractTrackingListener.php b/src/AbstractTrackingListener.php similarity index 53% rename from lib/Gedmo/AbstractTrackingListener.php rename to src/AbstractTrackingListener.php index 52a4beb24a..47293cded3 100644 --- a/lib/Gedmo/AbstractTrackingListener.php +++ b/src/AbstractTrackingListener.php @@ -1,56 +1,76 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo; use Doctrine\Common\EventArgs; -use Doctrine\Common\NotifyPropertyChanged; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\DBAL\Types\Type; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Types\Type as TypeODM; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\UnitOfWork; +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Event\ManagerEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\NotifyPropertyChanged; +use Doctrine\Persistence\ObjectManager; use Gedmo\Exception\UnexpectedValueException; use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Mapping\MappedEventSubscriber; /** - * The Timestampable listener handles the update of - * dates on creation and update. + * The AbstractTrackingListener provides generic functions for all listeners. + * + * @template TConfig of array + * @template TEventAdapter of AdapterInterface + * + * @template-extends MappedEventSubscriber * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ abstract class AbstractTrackingListener extends MappedEventSubscriber { /** - * Specifies the list of events to listen + * Specifies the list of events to listen on. * - * @return array + * @return string[] */ public function getSubscribedEvents() { - return array( + return [ 'prePersist', 'onFlush', 'loadClassMetadata', - ); + ]; } /** - * Maps additional metadata for the Entity + * Maps additional metadata for the object. * - * @param EventArgs $eventArgs + * @param LoadClassMetadataEventArgs $eventArgs + * + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { - $ea = $this->getEventAdapter($eventArgs); - $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata()); + $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } /** - * Looks for Timestampable objects being updated - * to update modification date + * Processes object updates when the manager is flushed. + * + * @param ManagerEventArgs $args * - * @param EventArgs $args + * @phpstan-param ManagerEventArgs $args * * @return void */ @@ -63,7 +83,7 @@ public function onFlush(EventArgs $args) $all = array_merge($ea->getScheduledObjectInsertions($uow), $ea->getScheduledObjectUpdates($uow)); foreach ($all as $object) { $meta = $om->getClassMetadata(get_class($object)); - if (!$config = $this->getConfiguration($om, $meta->name)) { + if (!$config = $this->getConfiguration($om, $meta->getName())) { continue; } $changeSet = $ea->getObjectChangeSet($uow, $object); @@ -71,9 +91,9 @@ public function onFlush(EventArgs $args) if ($uow->isScheduledForInsert($object) && isset($config['create'])) { foreach ($config['create'] as $field) { - // Field can not exist in change set, when persisting embedded document without parent for example + // Field can not exist in change set, i.e. when persisting an embedded object without a parent $new = array_key_exists($field, $changeSet) ? $changeSet[$field][1] : false; - if ($new === null) { // let manual values + if (null === $new) { // let manual values $needChanges = true; $this->updateField($object, $ea, $meta, $field); } @@ -84,7 +104,7 @@ public function onFlush(EventArgs $args) foreach ($config['update'] as $field) { $isInsertAndNull = $uow->isScheduledForInsert($object) && array_key_exists($field, $changeSet) - && $changeSet[$field][1] === null; + && null === $changeSet[$field][1]; if (!isset($changeSet[$field]) || $isInsertAndNull) { // let manual values $needChanges = true; $this->updateField($object, $ea, $meta, $field); @@ -100,37 +120,43 @@ public function onFlush(EventArgs $args) if (!is_array($options['trackedField'])) { $singleField = true; - $trackedFields = array($options['trackedField']); + $trackedFields = [$options['trackedField']]; } else { $singleField = false; $trackedFields = $options['trackedField']; } - foreach ($trackedFields as $tracked) { + foreach ($trackedFields as $trackedField) { $trackedChild = null; - $parts = explode('.', $tracked); + $tracked = null; + $parts = explode('.', $trackedField); if (isset($parts[1])) { $tracked = $parts[0]; $trackedChild = $parts[1]; } + if (!isset($tracked) || array_key_exists($trackedField, $changeSet)) { + $tracked = $trackedField; + $trackedChild = null; + } + if (isset($changeSet[$tracked])) { $changes = $changeSet[$tracked]; if (isset($trackedChild)) { $changingObject = $changes[1]; if (!is_object($changingObject)) { - throw new UnexpectedValueException( - "Field - [{$tracked}] is expected to be object in class - {$meta->name}" - ); + throw new UnexpectedValueException("Field - [{$tracked}] is expected to be object in class - {$meta->getName()}"); } $objectMeta = $om->getClassMetadata(get_class($changingObject)); $om->initializeObject($changingObject); - $value = $objectMeta->getReflectionProperty($trackedChild)->getValue($changingObject); + $value = $objectMeta->getFieldValue($changingObject, $trackedChild); } else { $value = $changes[1]; } - if (($singleField && in_array($value, (array) $options['value'])) || $options['value'] === null) { + $configuredValues = $this->getPhpValues($options['value'], $meta->getTypeOfField($tracked), $om); + + if (null === $configuredValues || ($singleField && in_array($value, $configuredValues, true))) { $needChanges = true; $this->updateField($object, $ea, $meta, $options['field']); } @@ -146,10 +172,11 @@ public function onFlush(EventArgs $args) } /** - * Checks for persisted Timestampable objects - * to update creation and modification dates + * Processes updates when an object is persisted in the manager. + * + * @param LifecycleEventArgs $args * - * @param EventArgs $args + * @phpstan-param LifecycleEventArgs $args * * @return void */ @@ -162,14 +189,14 @@ public function prePersist(EventArgs $args) if ($config = $this->getConfiguration($om, $meta->getName())) { if (isset($config['update'])) { foreach ($config['update'] as $field) { - if ($meta->getReflectionProperty($field)->getValue($object) === null) { // let manual values + if (null === $meta->getFieldValue($object, $field)) { // let manual values $this->updateField($object, $ea, $meta, $field); } } } if (isset($config['create'])) { foreach ($config['create'] as $field) { - if ($meta->getReflectionProperty($field)->getValue($object) === null) { // let manual values + if (null === $meta->getFieldValue($object, $field)) { // let manual values $this->updateField($object, $ea, $meta, $field); } } @@ -178,44 +205,84 @@ public function prePersist(EventArgs $args) } /** - * Get value for update field + * Get the value for an updated field. * - * @param ClassMetadata $meta - * @param string $field - * @param AdapterInterface $eventAdapter + * @param ClassMetadata $meta + * @param string $field + * @param TEventAdapter $eventAdapter + * + * @return mixed */ abstract protected function getFieldValue($meta, $field, $eventAdapter); /** - * Updates a field + * Updates a field. + * + * @param object $object + * @param TEventAdapter $eventAdapter + * @param ClassMetadata $meta + * @param string $field * - * @param object $object - * @param AdapterInterface $eventAdapter - * @param ClassMetadata $meta - * @param string $field + * @return void */ protected function updateField($object, $eventAdapter, $meta, $field) { - /** @var \Doctrine\Orm\Mapping\ClassMetadata|\Doctrine\ODM\MongoDB\Mapping\ClassMetadata $meta */ - $property = $meta->getReflectionProperty($field); - $oldValue = $property->getValue($object); + $oldValue = $meta->getFieldValue($object, $field); $newValue = $this->getFieldValue($meta, $field, $eventAdapter); // if field value is reference, persist object if ($meta->hasAssociation($field) && is_object($newValue) && !$eventAdapter->getObjectManager()->contains($newValue)) { $uow = $eventAdapter->getObjectManager()->getUnitOfWork(); - // Check to persist only when the entity isn't already managed, persists always for MongoDB - if(!($uow instanceof UnitOfWork) || $uow->getEntityState($newValue) !== UnitOfWork::STATE_MANAGED) { + // Check to persist only when the object isn't already managed, always persists for MongoDB + if (!($uow instanceof UnitOfWork) || UnitOfWork::STATE_MANAGED !== $uow->getEntityState($newValue)) { $eventAdapter->getObjectManager()->persist($newValue); } } - $property->setValue($object, $newValue); + $meta->setFieldValue($object, $field, $newValue); if ($object instanceof NotifyPropertyChanged) { $uow = $eventAdapter->getObjectManager()->getUnitOfWork(); $uow->propertyChanged($object, $field, $oldValue, $newValue); } } + + /** + * @param mixed $values + * + * @return mixed[]|null + */ + private function getPhpValues($values, ?string $type, ObjectManager $om): ?array + { + if (null === $values) { + return null; + } + + if (!is_array($values)) { + $values = [$values]; + } + + if (null !== $type) { + foreach ($values as $i => $value) { + if ($om instanceof DocumentManager) { + if (TypeODM::hasType($type)) { + $values[$i] = TypeODM::getType($type) + ->convertToPHPValue($value); + } else { + $values[$i] = $value; + } + } elseif ($om instanceof EntityManagerInterface) { + if (Type::hasType($type)) { + $values[$i] = $om->getConnection() + ->convertToPHPValue($value, $type); + } else { + $values[$i] = $value; + } + } + } + } + + return $values; + } } diff --git a/src/Blameable/Blameable.php b/src/Blameable/Blameable.php new file mode 100644 index 0000000000..7c1835cae5 --- /dev/null +++ b/src/Blameable/Blameable.php @@ -0,0 +1,54 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Blameable; + +/** + * Marker interface for objects which can be identified as blamable. + * + * @author Gediminas Morkevicius + */ +interface Blameable +{ + // blameable expects annotations on properties + + /* + * @Gedmo\Blameable(on="create") + * fields which should be updated on insert only + */ + + /* + * @Gedmo\Blameable(on="update") + * fields which should be updated on update and insert + */ + + /* + * @Gedmo\Blameable(on="change", field="field", value="value") + * fields which should be updated on changed "property" + * value and become equal to given "value" + */ + + /* + * @Gedmo\Blameable(on="change", field="field") + * fields which should be updated on changed "property" + */ + + /* + * @Gedmo\Blameable(on="change", fields={"field1", "field2"}) + * fields which should be updated if at least one of the given fields changed + */ + + /* + * example + * + * @Gedmo\Blameable(on="create") + * @Column(type="string") + * $created + */ +} diff --git a/src/Blameable/BlameableListener.php b/src/Blameable/BlameableListener.php new file mode 100644 index 0000000000..a39e4012e3 --- /dev/null +++ b/src/Blameable/BlameableListener.php @@ -0,0 +1,102 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Blameable; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\AbstractTrackingListener; +use Gedmo\Blameable\Mapping\Event\BlameableAdapter; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Tool\ActorProviderInterface; + +/** + * The Blameable listener handles the update of + * dates on creation and update. + * + * @phpstan-extends AbstractTrackingListener + * + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class BlameableListener extends AbstractTrackingListener +{ + protected ?ActorProviderInterface $actorProvider = null; + + /** + * @var mixed + */ + protected $user; + + /** + * Get the user value to set on a blameable field + * + * @param ClassMetadata $meta + * @param string $field + * @param BlameableAdapter $eventAdapter + * + * @return mixed + */ + public function getFieldValue($meta, $field, $eventAdapter) + { + $actor = $this->actorProvider instanceof ActorProviderInterface ? $this->actorProvider->getActor() : $this->user; + + if ($meta->hasAssociation($field)) { + if (null !== $actor && !is_object($actor)) { + throw new InvalidArgumentException('Blame is reference, user must be an object'); + } + + return $actor; + } + + // ok so it's not an association, then it is a string, or an object + if (is_object($actor)) { + if (method_exists($actor, 'getUserIdentifier')) { + return (string) $actor->getUserIdentifier(); + } + if (method_exists($actor, 'getUsername')) { + return (string) $actor->getUsername(); + } + if (method_exists($actor, '__toString')) { + return $actor->__toString(); + } + + throw new InvalidArgumentException('Field expects string, user must be a string, or object should have method getUserIdentifier, getUsername or __toString'); + } + + return $actor; + } + + /** + * Set an actor provider for the user value. + */ + public function setActorProvider(ActorProviderInterface $actorProvider): void + { + $this->actorProvider = $actorProvider; + } + + /** + * Set a user value to return. + * + * If an actor provider is also provided, it will take precedence over this value. + * + * @param mixed $user + * + * @return void + */ + public function setUserValue($user) + { + $this->user = $user; + } + + protected function getNamespace() + { + return __NAMESPACE__; + } +} diff --git a/src/Blameable/Mapping/Driver/Annotation.php b/src/Blameable/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..7dae3a81ca --- /dev/null +++ b/src/Blameable/Mapping/Driver/Annotation.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Blameable\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the blamable extension which reads extended metadata from annotations on a blamable class. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/lib/Gedmo/Blameable/Mapping/Driver/Annotation.php b/src/Blameable/Mapping/Driver/Attribute.php similarity index 60% rename from lib/Gedmo/Blameable/Mapping/Driver/Annotation.php rename to src/Blameable/Mapping/Driver/Attribute.php index 03e246db69..de2ce4158c 100644 --- a/lib/Gedmo/Blameable/Mapping/Driver/Annotation.php +++ b/src/Blameable/Mapping/Driver/Attribute.php @@ -1,86 +1,103 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Blameable\Mapping\Driver; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Blameable; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an annotation mapping driver for Blameable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Blameable - * extension. + * Mapping driver for the blameable extension which reads extended metadata from attributes on a blameable class. * * @author David Buchmann - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ -class Annotation extends AbstractAnnotationDriver +class Attribute extends AbstractAnnotationDriver { /** - * Annotation field is blameable + * Mapping object for the blameable extension. */ - const BLAMEABLE = 'Gedmo\\Mapping\\Annotation\\Blameable'; + public const BLAMEABLE = Blameable::class; /** * List of types which are valid for blame * - * @var array + * @var string[] */ - protected $validTypes = array( + protected $validTypes = [ 'one', 'string', 'int', - ); + 'ulid', + 'uuid', + 'ascii_string', + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { $class = $this->getMetaReflectionClass($meta); + // property annotations foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() || - $meta->isInheritedField($property->name) || - isset($meta->associationMappings[$property->name]['inherited']) + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) ) { continue; } + if ($blameable = $this->reader->getPropertyAnnotation($property, self::BLAMEABLE)) { + \assert($blameable instanceof Blameable); + $field = $property->getName(); if (!$meta->hasField($field) && !$meta->hasAssociation($field)) { - throw new InvalidMappingException("Unable to find blameable [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find blameable [{$field}] as mapped property in entity - {$meta->getName()}"); } + if ($meta->hasField($field)) { - if ( !$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a one-to-many relation in class - {$meta->name}"); + if (!$this->isValidField($meta, $field)) { + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a one-to-many relation in class - {$meta->getName()}"); } } else { // association - if (! $meta->isSingleValuedAssociation($field)) { - throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->name}"); + if (!$meta->isSingleValuedAssociation($field)) { + throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->getName()}"); } } - if (!in_array($blameable->on, array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + + if (!in_array($blameable->on, ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($blameable->on == 'change') { + + if ('change' === $blameable->on) { if (!isset($blameable->field)) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } + if (is_array($blameable->field) && isset($blameable->value)) { - throw new InvalidMappingException("Blameable extension does not support multiple value changeset detection yet."); + throw new InvalidMappingException('Blameable extension does not support multiple value changeset detection yet.'); } - $field = array( + + $field = [ 'field' => $field, 'trackedField' => $blameable->field, 'value' => $blameable->value, - ); + ]; } // properties are unique and mapper checks that, no risk here $config[$blameable->on][] = $field; } } + + return $config; } } diff --git a/lib/Gedmo/Blameable/Mapping/Driver/Xml.php b/src/Blameable/Mapping/Driver/Xml.php similarity index 66% rename from lib/Gedmo/Blameable/Mapping/Driver/Xml.php rename to src/Blameable/Mapping/Driver/Xml.php index 091de540f3..edd65846a4 100644 --- a/lib/Gedmo/Blameable/Mapping/Driver/Xml.php +++ b/src/Blameable/Mapping/Driver/Xml.php @@ -1,9 +1,17 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Blameable\Mapping\Driver; -use Gedmo\Mapping\Driver\Xml as BaseXml; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for Blameable @@ -12,66 +20,63 @@ * extension. * * @author David Buchmann - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ class Xml extends BaseXml { /** * List of types which are valid for blame * - * @var array + * @var string[] */ - private $validTypes = array( + private const VALID_TYPES = [ 'one', 'string', 'int', - ); + 'ulid', + 'uuid', + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { /** - * @var \SimpleXmlElement $mapping + * @var \SimpleXmlElement */ - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping->field)) { /** - * @var \SimpleXmlElement $fieldMapping + * @var \SimpleXmlElement */ foreach ($mapping->field as $fieldMapping) { $fieldMappingDoctrine = $fieldMapping; $fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($fieldMapping->blameable)) { /** - * @var \SimpleXmlElement $data + * @var \SimpleXmlElement */ $data = $fieldMapping->blameable; $field = $this->_getAttribute($fieldMappingDoctrine, 'name'); if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a reference in class - {$meta->name}"); + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a reference in class - {$meta->getName()}"); } - if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($this->_getAttribute($data, 'on') == 'change') { + if ('change' === $this->_getAttribute($data, 'on')) { if (!$this->_isAttributeSet($data, 'field')) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $this->_getAttribute($data, 'field'); - $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value' ) : null; - if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { - throw new InvalidMappingException("Blameable extension does not support multiple value changeset detection yet."); - } - $field = array( + $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value') : null; + $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, - ); + ]; } $config[$this->_getAttribute($data, 'on')][] = $field; } @@ -84,46 +89,45 @@ public function readExtendedMetadata($meta, array &$config) $fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($fieldMapping->blameable)) { $data = $fieldMapping->blameable; - if (! $meta->isSingleValuedAssociation($field)) { - throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->name}"); + if (!$meta->isSingleValuedAssociation($field)) { + throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->getName()}"); } - if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($this->_getAttribute($data, 'on') == 'change') { + if ('change' === $this->_getAttribute($data, 'on')) { if (!$this->_isAttributeSet($data, 'field')) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $this->_getAttribute($data, 'field'); - $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value' ) : null; - if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { - throw new InvalidMappingException("Blameable extension does not support multiple value changeset detection yet."); - } - $field = array( + $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value') : null; + $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, - ); + ]; } $config[$this->_getAttribute($data, 'on')][] = $field; } } } + + return $config; } /** * Checks if $field type is valid * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validTypes); + return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true); } } diff --git a/lib/Gedmo/Blameable/Mapping/Driver/Yaml.php b/src/Blameable/Mapping/Driver/Yaml.php similarity index 67% rename from lib/Gedmo/Blameable/Mapping/Driver/Yaml.php rename to src/Blameable/Mapping/Driver/Yaml.php index 56fb9b5cb0..76b20d6ebd 100644 --- a/lib/Gedmo/Blameable/Mapping/Driver/Yaml.php +++ b/src/Blameable/Mapping/Driver/Yaml.php @@ -1,10 +1,18 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Blameable\Mapping\Driver; -use Gedmo\Mapping\Driver\File; -use Gedmo\Mapping\Driver; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Blameable @@ -13,59 +21,62 @@ * extension. * * @author David Buchmann - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ class Yaml extends File implements Driver { - /** - * File extension - * @var string - */ - protected $_extension = '.dcm.yml'; - /** * List of types which are valid for blameable * - * @var array + * @var string[] */ - private $validTypes = array( + private const VALID_TYPES = [ 'one', 'string', 'int', - ); + 'ulid', + 'uuid', + ]; /** - * {@inheritDoc} + * File extension + * + * @var string */ + protected $_extension = '.dcm.yml'; + public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo']['blameable'])) { $mappingProperty = $fieldMapping['gedmo']['blameable']; if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a reference in class - {$meta->name}"); + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a reference in class - {$meta->getName()}"); } - if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($mappingProperty['on'] == 'change') { + if ('change' === $mappingProperty['on']) { if (!isset($mappingProperty['field'])) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $mappingProperty['field']; - $valueAttribute = isset($mappingProperty['value']) ? $mappingProperty['value'] : null; + $valueAttribute = $mappingProperty['value'] ?? null; if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { - throw new InvalidMappingException("Blameable extension does not support multiple value changeset detection yet."); + throw new InvalidMappingException('Blameable extension does not support multiple value changeset detection yet.'); } - $field = array( + $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, - ); + ]; } $config[$mappingProperty['on']][] = $field; } @@ -76,37 +87,36 @@ public function readExtendedMetadata($meta, array &$config) foreach ($mapping['manyToOne'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo']['blameable'])) { $mappingProperty = $fieldMapping['gedmo']['blameable']; - if (! $meta->isSingleValuedAssociation($field)) { - throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->name}"); + if (!$meta->isSingleValuedAssociation($field)) { + throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->getName()}"); } - if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($mappingProperty['on'] == 'change') { + if ('change' === $mappingProperty['on']) { if (!isset($mappingProperty['field'])) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $mappingProperty['field']; - $valueAttribute = isset($mappingProperty['value']) ? $mappingProperty['value'] : null; + $valueAttribute = $mappingProperty['value'] ?? null; if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { - throw new InvalidMappingException("Blameable extension does not support multiple value changeset detection yet."); + throw new InvalidMappingException('Blameable extension does not support multiple value changeset detection yet.'); } - $field = array( + $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, - ); + ]; } $config[$mappingProperty['on']][] = $field; } } } + + return $config; } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); @@ -115,15 +125,15 @@ protected function _loadMappingFile($file) /** * Checks if $field type is valid * - * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validTypes); + return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true); } } diff --git a/lib/Gedmo/Blameable/Mapping/Event/Adapter/ODM.php b/src/Blameable/Mapping/Event/Adapter/ODM.php similarity index 55% rename from lib/Gedmo/Blameable/Mapping/Event/Adapter/ODM.php rename to src/Blameable/Mapping/Event/Adapter/ODM.php index 298d2fa1a8..a89e967517 100644 --- a/lib/Gedmo/Blameable/Mapping/Event/Adapter/ODM.php +++ b/src/Blameable/Mapping/Event/Adapter/ODM.php @@ -1,16 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Blameable\Mapping\Event\Adapter; -use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\Blameable\Mapping\Event\BlameableAdapter; +use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; /** * Doctrine event adapter for ODM adapted * for Blameable behavior. * * @author David Buchmann - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ODM extends BaseAdapterODM implements BlameableAdapter { diff --git a/lib/Gedmo/Blameable/Mapping/Event/Adapter/ORM.php b/src/Blameable/Mapping/Event/Adapter/ORM.php similarity index 55% rename from lib/Gedmo/Blameable/Mapping/Event/Adapter/ORM.php rename to src/Blameable/Mapping/Event/Adapter/ORM.php index 226dec7d08..ea836b8bd6 100644 --- a/lib/Gedmo/Blameable/Mapping/Event/Adapter/ORM.php +++ b/src/Blameable/Mapping/Event/Adapter/ORM.php @@ -1,16 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Blameable\Mapping\Event\Adapter; -use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\Blameable\Mapping\Event\BlameableAdapter; +use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; /** * Doctrine event adapter for ORM adapted * for Blameable behavior. * * @author David Buchmann - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ORM extends BaseAdapterORM implements BlameableAdapter { diff --git a/src/Blameable/Mapping/Event/BlameableAdapter.php b/src/Blameable/Mapping/Event/BlameableAdapter.php new file mode 100644 index 0000000000..3c048f6dae --- /dev/null +++ b/src/Blameable/Mapping/Event/BlameableAdapter.php @@ -0,0 +1,21 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Blameable\Mapping\Event; + +use Gedmo\Mapping\Event\AdapterInterface; + +/** + * Doctrine event adapter for the Blameable extension. + * + * @author David Buchmann + */ +interface BlameableAdapter extends AdapterInterface +{ +} diff --git a/lib/Gedmo/Blameable/Traits/Blameable.php b/src/Blameable/Traits/Blameable.php similarity index 65% rename from lib/Gedmo/Blameable/Traits/Blameable.php rename to src/Blameable/Traits/Blameable.php index 88bfb9135f..97e237759e 100644 --- a/lib/Gedmo/Blameable/Traits/Blameable.php +++ b/src/Blameable/Traits/Blameable.php @@ -1,12 +1,20 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Blameable\Traits; /** - * Blameable Trait, usable with PHP >= 5.4 + * Trait for blamable objects. + * + * This implementation does not provide any mapping configurations. * * @author David Buchmann - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ trait Blameable { @@ -23,7 +31,8 @@ trait Blameable /** * Sets createdBy. * - * @param string $createdBy + * @param string $createdBy + * * @return $this */ public function setCreatedBy($createdBy) @@ -46,7 +55,8 @@ public function getCreatedBy() /** * Sets updatedBy. * - * @param string $updatedBy + * @param string $updatedBy + * * @return $this */ public function setUpdatedBy($updatedBy) diff --git a/lib/Gedmo/Blameable/Traits/BlameableDocument.php b/src/Blameable/Traits/BlameableDocument.php similarity index 58% rename from lib/Gedmo/Blameable/Traits/BlameableDocument.php rename to src/Blameable/Traits/BlameableDocument.php index d164da8b6f..8ee059cda1 100644 --- a/lib/Gedmo/Blameable/Traits/BlameableDocument.php +++ b/src/Blameable/Traits/BlameableDocument.php @@ -1,36 +1,54 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Blameable\Traits; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** - * Blameable Trait, usable with PHP >= 5.4 + * Trait for blamable objects. + * + * This implementation provides a mapping configuration for the Doctrine MongoDB ODM. * * @author David Buchmann - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ trait BlameableDocument { /** * @var string + * * @Gedmo\Blameable(on="create") - * @ODM\String + * + * @ODM\Field(type="string") */ + #[ODM\Field(type: Type::STRING)] + #[Gedmo\Blameable(on: 'create')] protected $createdBy; /** * @var string + * * @Gedmo\Blameable(on="update") - * @ODM\String + * + * @ODM\Field(type="string") */ + #[ODM\Field(type: Type::STRING)] + #[Gedmo\Blameable(on: 'update')] protected $updatedBy; /** * Sets createdBy. * - * @param string $createdBy + * @param string $createdBy + * * @return $this */ public function setCreatedBy($createdBy) @@ -53,7 +71,8 @@ public function getCreatedBy() /** * Sets updatedBy. * - * @param string $updatedBy + * @param string $updatedBy + * * @return $this */ public function setUpdatedBy($updatedBy) diff --git a/lib/Gedmo/Blameable/Traits/BlameableEntity.php b/src/Blameable/Traits/BlameableEntity.php similarity index 63% rename from lib/Gedmo/Blameable/Traits/BlameableEntity.php rename to src/Blameable/Traits/BlameableEntity.php index 837f8f9461..d507b139d5 100644 --- a/lib/Gedmo/Blameable/Traits/BlameableEntity.php +++ b/src/Blameable/Traits/BlameableEntity.php @@ -1,36 +1,53 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Blameable\Traits; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** - * Blameable Trait, usable with PHP >= 5.4 + * Trait for blamable objects. + * + * This implementation provides a mapping configuration for the Doctrine ORM. * * @author David Buchmann - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ trait BlameableEntity { /** * @var string + * * @Gedmo\Blameable(on="create") + * * @ORM\Column(nullable=true) */ + #[ORM\Column(nullable: true)] + #[Gedmo\Blameable(on: 'create')] protected $createdBy; /** * @var string + * * @Gedmo\Blameable(on="update") + * * @ORM\Column(nullable=true) */ + #[ORM\Column(nullable: true)] + #[Gedmo\Blameable(on: 'update')] protected $updatedBy; /** * Sets createdBy. * - * @param string $createdBy + * @param string $createdBy + * * @return $this */ public function setCreatedBy($createdBy) @@ -53,7 +70,8 @@ public function getCreatedBy() /** * Sets updatedBy. * - * @param string $updatedBy + * @param string $updatedBy + * * @return $this */ public function setUpdatedBy($updatedBy) diff --git a/src/DoctrineExtensions.php b/src/DoctrineExtensions.php new file mode 100644 index 0000000000..71130b3cac --- /dev/null +++ b/src/DoctrineExtensions.php @@ -0,0 +1,145 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\PsrCachedReader; +use Doctrine\Common\Annotations\Reader; +use Doctrine\Deprecations\Deprecation; +use Doctrine\ODM\MongoDB\Mapping\Driver as DriverMongodbODM; +use Doctrine\ORM\Mapping\Driver as DriverORM; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\Exception\RuntimeException; +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +/** + * Version class allows checking the required dependencies + * and the current version of the Doctrine Extensions library. + * + * @author Gediminas Morkevicius + */ +final class DoctrineExtensions +{ + /** + * Current version of extensions + */ + public const VERSION = '3.22.0'; + + /** + * Hooks all extension metadata mapping drivers into + * the given driver chain of drivers for the ORM. + */ + public static function registerMappingIntoDriverChainORM(MappingDriverChain $driverChain, ?Reader $reader = null): void + { + $paths = [ + __DIR__.'/Translatable/Entity', + __DIR__.'/Loggable/Entity', + __DIR__.'/Tree/Entity', + ]; + + if (\PHP_VERSION_ID >= 80000) { + $driver = new DriverORM\AttributeDriver($paths); + } else { + $driver = new DriverORM\AnnotationDriver($reader ?? self::createAnnotationReader(), $paths); + } + + $driverChain->addDriver($driver, 'Gedmo'); + } + + /** + * Hooks only superclass extension metadata mapping drivers into + * the given driver chain of drivers for the ORM. + */ + public static function registerAbstractMappingIntoDriverChainORM(MappingDriverChain $driverChain, ?Reader $reader = null): void + { + $paths = [ + __DIR__.'/Translatable/Entity/MappedSuperclass', + __DIR__.'/Loggable/Entity/MappedSuperclass', + __DIR__.'/Tree/Entity/MappedSuperclass', + ]; + + if (\PHP_VERSION_ID >= 80000) { + $driver = new DriverORM\AttributeDriver($paths); + } else { + $driver = new DriverORM\AnnotationDriver($reader ?? self::createAnnotationReader(), $paths); + } + + $driverChain->addDriver($driver, 'Gedmo'); + } + + /** + * Hooks all extension metadata mapping drivers into + * the given driver chain of drivers for the MongoDB ODM. + */ + public static function registerMappingIntoDriverChainMongodbODM(MappingDriverChain $driverChain, ?Reader $reader = null): void + { + $paths = [ + __DIR__.'/Translatable/Document', + __DIR__.'/Loggable/Document', + ]; + + if (\PHP_VERSION_ID >= 80000) { + $driver = new DriverMongodbODM\AttributeDriver($paths); + } else { + $driver = new DriverMongodbODM\AnnotationDriver($reader ?? self::createAnnotationReader(), $paths); + } + + $driverChain->addDriver($driver, 'Gedmo'); + } + + /** + * Hooks only superclass extension metadata mapping drivers into + * the given driver chain of drivers for the MongoDB ODM. + */ + public static function registerAbstractMappingIntoDriverChainMongodbODM(MappingDriverChain $driverChain, ?Reader $reader = null): void + { + $paths = [ + __DIR__.'/Translatable/Document/MappedSuperclass', + __DIR__.'/Loggable/Document/MappedSuperclass', + ]; + + if (\PHP_VERSION_ID >= 80000) { + $driver = new DriverMongodbODM\AttributeDriver($paths); + } else { + $driver = new DriverMongodbODM\AnnotationDriver($reader ?? self::createAnnotationReader(), $paths); + } + + $driverChain->addDriver($driver, 'Gedmo'); + } + + /** + * Registers all extension annotations. + * + * @deprecated to be removed in 4.0, annotation classes are autoloaded instead + */ + public static function registerAnnotations(): void + { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2558', + '"%s()" is deprecated since gedmo/doctrine-extensions 3.11 and will be removed in version 4.0.', + __METHOD__ + ); + + // Purposefully no-op'd, all supported versions of `doctrine/annotations` support autoloading + } + + /** + * @throws RuntimeException if running PHP 7 and the `doctrine/annotations` package is not installed + */ + private static function createAnnotationReader(): PsrCachedReader + { + if (!class_exists(AnnotationReader::class)) { + throw new RuntimeException(sprintf('The "%1$s" class requires the "doctrine/annotations" package to use annotations but it is not available. Try running "composer require doctrine/annotations" or upgrade to PHP 8 to use attributes.', self::class)); + } + + return new PsrCachedReader(new AnnotationReader(), new ArrayAdapter()); + } +} diff --git a/src/Exception.php b/src/Exception.php new file mode 100644 index 0000000000..2fc209a84e --- /dev/null +++ b/src/Exception.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo; + +/** + * Marker interface for all exceptions in the Doctrine Extensions package. + * + * @author Gediminas Morkevicius + */ +interface Exception extends \Throwable +{ + /* + * Following best practices for PHP5.3 package exceptions. + * All exceptions thrown in this package will have to implement this interface + */ +} diff --git a/src/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php new file mode 100644 index 0000000000..6ce3e3478b --- /dev/null +++ b/src/Exception/BadMethodCallException.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * BadMethodCallException + * + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class BadMethodCallException extends \BadMethodCallException implements Exception +{ +} diff --git a/src/Exception/FeatureNotImplementedException.php b/src/Exception/FeatureNotImplementedException.php new file mode 100644 index 0000000000..8d5296bdc7 --- /dev/null +++ b/src/Exception/FeatureNotImplementedException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * FeatureNotImplementedException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class FeatureNotImplementedException extends \RuntimeException implements Exception +{ +} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000..5a74245e69 --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * InvalidArgumentException + * + * @author Gediminas Morkevicius + */ +class InvalidArgumentException extends \InvalidArgumentException implements Exception +{ +} diff --git a/src/Exception/InvalidMappingException.php b/src/Exception/InvalidMappingException.php new file mode 100644 index 0000000000..0df59a1765 --- /dev/null +++ b/src/Exception/InvalidMappingException.php @@ -0,0 +1,26 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * InvalidMappingException + * + * Triggered when mapping user argument is not + * valid or incomplete. + * + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class InvalidMappingException extends InvalidArgumentException implements Exception +{ +} diff --git a/src/Exception/ReferenceIntegrityStrictException.php b/src/Exception/ReferenceIntegrityStrictException.php new file mode 100644 index 0000000000..53c0d09522 --- /dev/null +++ b/src/Exception/ReferenceIntegrityStrictException.php @@ -0,0 +1,21 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +/** + * ReferenceIntegrityStrictException + * + * @author Evert Harmeling + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class ReferenceIntegrityStrictException extends RuntimeException +{ +} diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php new file mode 100644 index 0000000000..dcb860ed65 --- /dev/null +++ b/src/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * RuntimeException + * + * @author Gediminas Morkevicius + */ +class RuntimeException extends \RuntimeException implements Exception +{ +} diff --git a/src/Exception/TreeLockingException.php b/src/Exception/TreeLockingException.php new file mode 100644 index 0000000000..2a7aa417c7 --- /dev/null +++ b/src/Exception/TreeLockingException.php @@ -0,0 +1,22 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +/** + * TreeLockingException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class TreeLockingException extends RuntimeException +{ +} diff --git a/src/Exception/UnexpectedValueException.php b/src/Exception/UnexpectedValueException.php new file mode 100644 index 0000000000..9d1fc344cf --- /dev/null +++ b/src/Exception/UnexpectedValueException.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UnexpectedValueException + * + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UnexpectedValueException extends \UnexpectedValueException implements Exception +{ +} diff --git a/src/Exception/UnsupportedObjectManagerException.php b/src/Exception/UnsupportedObjectManagerException.php new file mode 100644 index 0000000000..e35e8b2221 --- /dev/null +++ b/src/Exception/UnsupportedObjectManagerException.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UnsupportedObjectManager + * + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UnsupportedObjectManagerException extends InvalidArgumentException implements Exception +{ +} diff --git a/src/Exception/UploadableCantWriteException.php b/src/Exception/UploadableCantWriteException.php new file mode 100644 index 0000000000..032a912379 --- /dev/null +++ b/src/Exception/UploadableCantWriteException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableCantWriteException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableCantWriteException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableCouldntGuessMimeTypeException.php b/src/Exception/UploadableCouldntGuessMimeTypeException.php new file mode 100644 index 0000000000..59b7988791 --- /dev/null +++ b/src/Exception/UploadableCouldntGuessMimeTypeException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableCouldntGuessMimeTypeException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableCouldntGuessMimeTypeException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableDirectoryNotFoundException.php b/src/Exception/UploadableDirectoryNotFoundException.php new file mode 100644 index 0000000000..c4208b8e37 --- /dev/null +++ b/src/Exception/UploadableDirectoryNotFoundException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableDirectoryNotFoundException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableDirectoryNotFoundException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableException.php b/src/Exception/UploadableException.php new file mode 100644 index 0000000000..11a450ede2 --- /dev/null +++ b/src/Exception/UploadableException.php @@ -0,0 +1,22 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +class UploadableException extends RuntimeException implements Exception +{ +} diff --git a/src/Exception/UploadableExtensionException.php b/src/Exception/UploadableExtensionException.php new file mode 100644 index 0000000000..1e365fb1e9 --- /dev/null +++ b/src/Exception/UploadableExtensionException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableExtensionException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableExtensionException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableFileAlreadyExistsException.php b/src/Exception/UploadableFileAlreadyExistsException.php new file mode 100644 index 0000000000..36f6eb8f8d --- /dev/null +++ b/src/Exception/UploadableFileAlreadyExistsException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableFileAlreadyExistsException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableFileAlreadyExistsException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableFileNotReadableException.php b/src/Exception/UploadableFileNotReadableException.php new file mode 100644 index 0000000000..9bede35e0b --- /dev/null +++ b/src/Exception/UploadableFileNotReadableException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableFileNotReadableException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableFileNotReadableException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableFormSizeException.php b/src/Exception/UploadableFormSizeException.php new file mode 100644 index 0000000000..71ddc48099 --- /dev/null +++ b/src/Exception/UploadableFormSizeException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableFormSizeException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableFormSizeException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableIniSizeException.php b/src/Exception/UploadableIniSizeException.php new file mode 100644 index 0000000000..14300d0a68 --- /dev/null +++ b/src/Exception/UploadableIniSizeException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableIniSizeException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableIniSizeException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableInvalidFileException.php b/src/Exception/UploadableInvalidFileException.php new file mode 100644 index 0000000000..c36edf128a --- /dev/null +++ b/src/Exception/UploadableInvalidFileException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableInvalidFileException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableInvalidFileException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableInvalidMimeTypeException.php b/src/Exception/UploadableInvalidMimeTypeException.php new file mode 100644 index 0000000000..d90aca708d --- /dev/null +++ b/src/Exception/UploadableInvalidMimeTypeException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableInvalidMimeTypeException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableInvalidMimeTypeException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableInvalidPathException.php b/src/Exception/UploadableInvalidPathException.php new file mode 100644 index 0000000000..c767d2a595 --- /dev/null +++ b/src/Exception/UploadableInvalidPathException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableInvalidPathException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableInvalidPathException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableMaxSizeException.php b/src/Exception/UploadableMaxSizeException.php new file mode 100644 index 0000000000..3b3e68be92 --- /dev/null +++ b/src/Exception/UploadableMaxSizeException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableMaxSizeException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableMaxSizeException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableNoFileException.php b/src/Exception/UploadableNoFileException.php new file mode 100644 index 0000000000..2ca3fae1d0 --- /dev/null +++ b/src/Exception/UploadableNoFileException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableNoFileException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableNoFileException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableNoPathDefinedException.php b/src/Exception/UploadableNoPathDefinedException.php new file mode 100644 index 0000000000..51db45ea92 --- /dev/null +++ b/src/Exception/UploadableNoPathDefinedException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableNoPathDefinedException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableNoPathDefinedException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableNoTmpDirException.php b/src/Exception/UploadableNoTmpDirException.php new file mode 100644 index 0000000000..b21303a630 --- /dev/null +++ b/src/Exception/UploadableNoTmpDirException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableNoTmpDirException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableNoTmpDirException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadablePartialException.php b/src/Exception/UploadablePartialException.php new file mode 100644 index 0000000000..5b2974e5ac --- /dev/null +++ b/src/Exception/UploadablePartialException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadablePartialException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadablePartialException extends UploadableException implements Exception +{ +} diff --git a/src/Exception/UploadableUploadException.php b/src/Exception/UploadableUploadException.php new file mode 100644 index 0000000000..9ccf4f967c --- /dev/null +++ b/src/Exception/UploadableUploadException.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Exception; + +use Gedmo\Exception; + +/** + * UploadableUploadException + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadableUploadException extends UploadableException implements Exception +{ +} diff --git a/src/IpTraceable/IpTraceable.php b/src/IpTraceable/IpTraceable.php new file mode 100644 index 0000000000..8c526d01a4 --- /dev/null +++ b/src/IpTraceable/IpTraceable.php @@ -0,0 +1,54 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\IpTraceable; + +/** + * Marker interface for objects which can be identified as IP traceable. + * + * @author Pierre-Charles Bertineau + */ +interface IpTraceable +{ + // ipTraceable expects annotations on properties + + /* + * @Gedmo\IpTraceable(on="create") + * strings which should be updated on insert only + */ + + /* + * @Gedmo\IpTraceable(on="update") + * strings which should be updated on update and insert + */ + + /* + * @Gedmo\IpTraceable(on="change", field="field", value="value") + * strings which should be updated on changed "property" + * value and become equal to given "value" + */ + + /* + * @Gedmo\IpTraceable(on="change", field="field") + * strings which should be updated on changed "property" + */ + + /* + * @Gedmo\IpTraceable(on="change", fields={"field1", "field2"}) + * strings which should be updated if at least one of the given fields changed + */ + + /* + * example + * + * @Gedmo\IpTraceable(on="create") + * @Column(type="string") + * $created + */ +} diff --git a/src/IpTraceable/IpTraceableListener.php b/src/IpTraceable/IpTraceableListener.php new file mode 100644 index 0000000000..f4f7764545 --- /dev/null +++ b/src/IpTraceable/IpTraceableListener.php @@ -0,0 +1,87 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\IpTraceable; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\AbstractTrackingListener; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\IpTraceable\Mapping\Event\IpTraceableAdapter; +use Gedmo\Tool\IpAddressProviderInterface; + +/** + * The IpTraceable listener handles the update of + * IPs on creation and update. + * + * @phpstan-extends AbstractTrackingListener + * + * @author Pierre-Charles Bertineau + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class IpTraceableListener extends AbstractTrackingListener +{ + protected ?IpAddressProviderInterface $ipAddressProvider = null; + + /** + * @var string|null + */ + protected $ip; + + /** + * Get the IP address value to set on an IP address field + * + * @param ClassMetadata $meta + * @param string $field + * @param IpTraceableAdapter $eventAdapter + * + * @return string|null + */ + public function getFieldValue($meta, $field, $eventAdapter) + { + if ($this->ipAddressProvider instanceof IpAddressProviderInterface) { + return $this->ipAddressProvider->getAddress(); + } + + return $this->ip; + } + + /** + * Set an IP address provider for the IP address value. + */ + public function setIpAddressProvider(IpAddressProviderInterface $ipAddressProvider): void + { + $this->ipAddressProvider = $ipAddressProvider; + } + + /** + * Set an IP address value to return. + * + * If an IP address provider is also provided, it will take precedence over this value. + * + * @param string|null $ip + * + * @throws InvalidArgumentException + * + * @return void + */ + public function setIpValue($ip = null) + { + if (isset($ip) && false === filter_var($ip, FILTER_VALIDATE_IP)) { + throw new InvalidArgumentException("ip address is not valid $ip"); + } + + $this->ip = $ip; + } + + protected function getNamespace() + { + return __NAMESPACE__; + } +} diff --git a/src/IpTraceable/Mapping/Driver/Annotation.php b/src/IpTraceable/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..03d7537db1 --- /dev/null +++ b/src/IpTraceable/Mapping/Driver/Annotation.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\IpTraceable\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the IP traceable extension which reads extended metadata from annotations on an IP traceable class. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/lib/Gedmo/IpTraceable/Mapping/Driver/Annotation.php b/src/IpTraceable/Mapping/Driver/Attribute.php similarity index 57% rename from lib/Gedmo/IpTraceable/Mapping/Driver/Annotation.php rename to src/IpTraceable/Mapping/Driver/Attribute.php index 206ced25b9..19a8ee23bc 100644 --- a/lib/Gedmo/IpTraceable/Mapping/Driver/Annotation.php +++ b/src/IpTraceable/Mapping/Driver/Attribute.php @@ -1,77 +1,93 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\IpTraceable\Mapping\Driver; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\IpTraceable; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an annotation mapping driver for IpTraceable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for IpTraceable - * extension. + * Mapping driver for the IP traceable extension which reads extended metadata from attributes on an IP traceable class. * * @author Pierre-Charles Bertineau - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ -class Annotation extends AbstractAnnotationDriver +class Attribute extends AbstractAnnotationDriver { /** - * Annotation field is ipTraceable + * Mapping object for the IP traceable extension. */ - const IP_TRACEABLE = 'Gedmo\\Mapping\\Annotation\\IpTraceable'; + public const IP_TRACEABLE = IpTraceable::class; /** * List of types which are valid for IP * - * @var array + * @var string[] */ - protected $validTypes = array( + protected $validTypes = [ 'string', - ); + 'ascii_string', + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { $class = $this->getMetaReflectionClass($meta); + // property annotations foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() || - $meta->isInheritedField($property->name) || - isset($meta->associationMappings[$property->name]['inherited']) + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) ) { continue; } + if ($ipTraceable = $this->reader->getPropertyAnnotation($property, self::IP_TRACEABLE)) { + \assert($ipTraceable instanceof IpTraceable); + $field = $property->getName(); if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find ipTraceable [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find ipTraceable [{$field}] as mapped property in entity - {$meta->getName()}"); } - if ($meta->hasField($field) && !$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' - {$meta->name}"); + + if (!$this->isValidField($meta, $field)) { + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' - {$meta->getName()}"); } - if (!in_array($ipTraceable->on, array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + + if (!in_array($ipTraceable->on, ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($ipTraceable->on == 'change') { + + if ('change' === $ipTraceable->on) { if (!isset($ipTraceable->field)) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } + if (is_array($ipTraceable->field) && isset($ipTraceable->value)) { - throw new InvalidMappingException("IpTraceable extension does not support multiple value changeset detection yet."); + throw new InvalidMappingException('IpTraceable extension does not support multiple value changeset detection yet.'); } - $field = array( + + $field = [ 'field' => $field, 'trackedField' => $ipTraceable->field, 'value' => $ipTraceable->value, - ); + ]; } + // properties are unique and mapper checks that, no risk here $config[$ipTraceable->on][] = $field; } } + + return $config; } } diff --git a/lib/Gedmo/IpTraceable/Mapping/Driver/Xml.php b/src/IpTraceable/Mapping/Driver/Xml.php similarity index 67% rename from lib/Gedmo/IpTraceable/Mapping/Driver/Xml.php rename to src/IpTraceable/Mapping/Driver/Xml.php index 2140a7e785..ef5e34e7ad 100644 --- a/lib/Gedmo/IpTraceable/Mapping/Driver/Xml.php +++ b/src/IpTraceable/Mapping/Driver/Xml.php @@ -1,9 +1,17 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\IpTraceable\Mapping\Driver; -use Gedmo\Mapping\Driver\Xml as BaseXml; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for IpTraceable @@ -14,64 +22,59 @@ * @author Gediminas Morkevicius * @author Miha Vrhovnik * @author Pierre-Charles Bertineau - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ class Xml extends BaseXml { /** * List of types which are valid for IP * - * @var array + * @var string[] */ - private $validTypes = array( + private const VALID_TYPES = [ 'string', - ); + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { /** - * @var \SimpleXmlElement $mapping + * @var \SimpleXmlElement */ - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping->field)) { /** - * @var \SimpleXmlElement $fieldMapping + * @var \SimpleXmlElement */ foreach ($mapping->field as $fieldMapping) { $fieldMappingDoctrine = $fieldMapping; $fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($fieldMapping->{'ip-traceable'})) { /** - * @var \SimpleXmlElement $data + * @var \SimpleXmlElement */ $data = $fieldMapping->{'ip-traceable'}; $field = $this->_getAttribute($fieldMappingDoctrine, 'name'); if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' in class - {$meta->name}"); + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' in class - {$meta->getName()}"); } - if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($this->_getAttribute($data, 'on') == 'change') { + if ('change' === $this->_getAttribute($data, 'on')) { if (!$this->_isAttributeSet($data, 'field')) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $this->_getAttribute($data, 'field'); - $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value' ) : null; - if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { - throw new InvalidMappingException("IpTraceable extension does not support multiple value changeset detection yet."); - } - $field = array( + $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value') : null; + $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, - ); + ]; } $config[$this->_getAttribute($data, 'on')][] = $field; } @@ -84,50 +87,49 @@ public function readExtendedMetadata($meta, array &$config) $fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($fieldMapping->{'ip-traceable'})) { /** - * @var \SimpleXmlElement $data + * @var \SimpleXmlElement */ $data = $fieldMapping->{'ip-traceable'}; - if (! $meta->isSingleValuedAssociation($field)) { - throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->name}"); + if (!$meta->isSingleValuedAssociation($field)) { + throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->getName()}"); } - if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($this->_getAttribute($data, 'on') == 'change') { + if ('change' === $this->_getAttribute($data, 'on')) { if (!$this->_isAttributeSet($data, 'field')) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $this->_getAttribute($data, 'field'); - $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value' ) : null; - if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { - throw new InvalidMappingException("IpTraceable extension does not support multiple value changeset detection yet."); - } - $field = array( + $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value') : null; + $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, - ); + ]; } $config[$this->_getAttribute($data, 'on')][] = $field; } } + + return $config; } } /** * Checks if $field type is valid * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validTypes); + return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true); } } diff --git a/lib/Gedmo/IpTraceable/Mapping/Driver/Yaml.php b/src/IpTraceable/Mapping/Driver/Yaml.php similarity index 68% rename from lib/Gedmo/IpTraceable/Mapping/Driver/Yaml.php rename to src/IpTraceable/Mapping/Driver/Yaml.php index 2f33c4a626..fb6b9c050d 100644 --- a/lib/Gedmo/IpTraceable/Mapping/Driver/Yaml.php +++ b/src/IpTraceable/Mapping/Driver/Yaml.php @@ -1,10 +1,18 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\IpTraceable\Mapping\Driver; -use Gedmo\Mapping\Driver\File; -use Gedmo\Mapping\Driver; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for IpTraceable @@ -13,57 +21,58 @@ * extension. * * @author Pierre-Charles Bertineau - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ class Yaml extends File implements Driver { - /** - * File extension - * @var string - */ - protected $_extension = '.dcm.yml'; - /** * List of types which are valid for IP * - * @var array + * @var string[] */ - private $validTypes = array( + private const VALID_TYPES = [ 'string', - ); + ]; /** - * {@inheritDoc} + * File extension + * + * @var string */ + protected $_extension = '.dcm.yml'; + public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo']['ipTraceable'])) { $mappingProperty = $fieldMapping['gedmo']['ipTraceable']; if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' in class - {$meta->name}"); + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' in class - {$meta->getName()}"); } - if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($mappingProperty['on'] == 'change') { + if ('change' === $mappingProperty['on']) { if (!isset($mappingProperty['field'])) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $mappingProperty['field']; - $valueAttribute = isset($mappingProperty['value']) ? $mappingProperty['value'] : null; + $valueAttribute = $mappingProperty['value'] ?? null; if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { - throw new InvalidMappingException("IpTraceable extension does not support multiple value changeset detection yet."); + throw new InvalidMappingException('IpTraceable extension does not support multiple value changeset detection yet.'); } - $field = array( + $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, - ); + ]; } $config[$mappingProperty['on']][] = $field; } @@ -74,37 +83,36 @@ public function readExtendedMetadata($meta, array &$config) foreach ($mapping['manyToOne'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo']['ipTraceable'])) { $mappingProperty = $fieldMapping['gedmo']['ipTraceable']; - if (! $meta->isSingleValuedAssociation($field)) { - throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->name}"); + if (!$meta->isSingleValuedAssociation($field)) { + throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->getName()}"); } - if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($mappingProperty['on'] == 'change') { + if ('change' === $mappingProperty['on']) { if (!isset($mappingProperty['field'])) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $mappingProperty['field']; - $valueAttribute = isset($mappingProperty['value']) ? $mappingProperty['value'] : null; + $valueAttribute = $mappingProperty['value'] ?? null; if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { - throw new InvalidMappingException("IpTraceable extension does not support multiple value changeset detection yet."); + throw new InvalidMappingException('IpTraceable extension does not support multiple value changeset detection yet.'); } - $field = array( + $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, - ); + ]; } $config[$mappingProperty['on']][] = $field; } } + + return $config; } } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); @@ -113,15 +121,15 @@ protected function _loadMappingFile($file) /** * Checks if $field type is valid * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validTypes); + return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true); } } diff --git a/lib/Gedmo/IpTraceable/Mapping/Event/Adapter/ODM.php b/src/IpTraceable/Mapping/Event/Adapter/ODM.php similarity index 57% rename from lib/Gedmo/IpTraceable/Mapping/Event/Adapter/ODM.php rename to src/IpTraceable/Mapping/Event/Adapter/ODM.php index 8202345f90..5520865903 100644 --- a/lib/Gedmo/IpTraceable/Mapping/Event/Adapter/ODM.php +++ b/src/IpTraceable/Mapping/Event/Adapter/ODM.php @@ -1,16 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\IpTraceable\Mapping\Event\Adapter; -use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\IpTraceable\Mapping\Event\IpTraceableAdapter; +use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; /** * Doctrine event adapter for ODM adapted * for IpTraceable behavior * * @author Pierre-Charles Bertineau - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ODM extends BaseAdapterODM implements IpTraceableAdapter { diff --git a/lib/Gedmo/IpTraceable/Mapping/Event/Adapter/ORM.php b/src/IpTraceable/Mapping/Event/Adapter/ORM.php similarity index 57% rename from lib/Gedmo/IpTraceable/Mapping/Event/Adapter/ORM.php rename to src/IpTraceable/Mapping/Event/Adapter/ORM.php index 784264afdf..0e22a45376 100644 --- a/lib/Gedmo/IpTraceable/Mapping/Event/Adapter/ORM.php +++ b/src/IpTraceable/Mapping/Event/Adapter/ORM.php @@ -1,16 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\IpTraceable\Mapping\Event\Adapter; -use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\IpTraceable\Mapping\Event\IpTraceableAdapter; +use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; /** * Doctrine event adapter for ORM adapted * for IpTraceable behavior * * @author Pierre-Charles Bertineau - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ORM extends BaseAdapterORM implements IpTraceableAdapter { diff --git a/src/IpTraceable/Mapping/Event/IpTraceableAdapter.php b/src/IpTraceable/Mapping/Event/IpTraceableAdapter.php new file mode 100644 index 0000000000..7a93a501d4 --- /dev/null +++ b/src/IpTraceable/Mapping/Event/IpTraceableAdapter.php @@ -0,0 +1,21 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\IpTraceable\Mapping\Event; + +use Gedmo\Mapping\Event\AdapterInterface; + +/** + * Doctrine event adapter for the IpTraceable extension. + * + * @author Pierre-Charles Bertineau + */ +interface IpTraceableAdapter extends AdapterInterface +{ +} diff --git a/lib/Gedmo/IpTraceable/Traits/IpTraceable.php b/src/IpTraceable/Traits/IpTraceable.php similarity index 67% rename from lib/Gedmo/IpTraceable/Traits/IpTraceable.php rename to src/IpTraceable/Traits/IpTraceable.php index ecfd362022..26497ac2e9 100644 --- a/lib/Gedmo/IpTraceable/Traits/IpTraceable.php +++ b/src/IpTraceable/Traits/IpTraceable.php @@ -1,12 +1,20 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\IpTraceable\Traits; /** - * IpTraceable Trait, usable with PHP >= 5.4 + * Trait for IP traceable objects. + * + * This implementation does not provide any mapping configurations. * * @author Pierre-Charles Bertineau - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ trait IpTraceable { @@ -23,7 +31,8 @@ trait IpTraceable /** * Sets createdFromIp. * - * @param string $createdFromIp + * @param string $createdFromIp + * * @return $this */ public function setCreatedFromIp($createdFromIp) @@ -46,7 +55,8 @@ public function getCreatedFromIp() /** * Sets updatedFromIp. * - * @param string $updatedFromIp + * @param string $updatedFromIp + * * @return $this */ public function setUpdatedFromIp($updatedFromIp) diff --git a/lib/Gedmo/IpTraceable/Traits/IpTraceableDocument.php b/src/IpTraceable/Traits/IpTraceableDocument.php similarity index 60% rename from lib/Gedmo/IpTraceable/Traits/IpTraceableDocument.php rename to src/IpTraceable/Traits/IpTraceableDocument.php index 7ee0e851d4..7cd6a372fd 100644 --- a/lib/Gedmo/IpTraceable/Traits/IpTraceableDocument.php +++ b/src/IpTraceable/Traits/IpTraceableDocument.php @@ -1,36 +1,54 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\IpTraceable\Traits; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** - * IpTraceable Trait, usable with PHP >= 5.4 + * Trait for IP traceable objects. + * + * This implementation provides a mapping configuration for the Doctrine MongoDB ODM. * * @author Pierre-Charles Bertineau - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ trait IpTraceableDocument { /** * @var string + * * @Gedmo\IpTraceable(on="create") - * @ODM\String + * + * @ODM\Field(type="string") */ + #[ODM\Field(type: Type::STRING)] + #[Gedmo\IpTraceable(on: 'create')] protected $createdFromIp; /** * @var string + * * @Gedmo\IpTraceable(on="update") - * @ODM\String + * + * @ODM\Field(type="string") */ + #[ODM\Field(type: Type::STRING)] + #[Gedmo\IpTraceable(on: 'update')] protected $updatedFromIp; /** * Sets createdFromIp. * - * @param string $createdFromIp + * @param string $createdFromIp + * * @return $this */ public function setCreatedFromIp($createdFromIp) @@ -53,7 +71,8 @@ public function getCreatedFromIp() /** * Sets updatedFromIp. * - * @param string $updatedFromIp + * @param string $updatedFromIp + * * @return $this */ public function setUpdatedFromIp($updatedFromIp) diff --git a/lib/Gedmo/IpTraceable/Traits/IpTraceableEntity.php b/src/IpTraceable/Traits/IpTraceableEntity.php similarity index 64% rename from lib/Gedmo/IpTraceable/Traits/IpTraceableEntity.php rename to src/IpTraceable/Traits/IpTraceableEntity.php index e5f4c8de5c..11fc85192b 100644 --- a/lib/Gedmo/IpTraceable/Traits/IpTraceableEntity.php +++ b/src/IpTraceable/Traits/IpTraceableEntity.php @@ -1,36 +1,53 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\IpTraceable\Traits; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** - * IpTraceable Trait, usable with PHP >= 5.4 + * Trait for IP traceable objects. + * + * This implementation provides a mapping configuration for the Doctrine ORM. * * @author Pierre-Charles Bertineau - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ trait IpTraceableEntity { /** * @var string + * * @Gedmo\IpTraceable(on="create") + * * @ORM\Column(length=45, nullable=true) */ + #[ORM\Column(length: 45, nullable: true)] + #[Gedmo\IpTraceable(on: 'create')] protected $createdFromIp; /** * @var string + * * @Gedmo\IpTraceable(on="update") + * * @ORM\Column(length=45, nullable=true) */ + #[ORM\Column(length: 45, nullable: true)] + #[Gedmo\IpTraceable(on: 'update')] protected $updatedFromIp; /** * Sets createdFromIp. * - * @param string $createdFromIp + * @param string $createdFromIp + * * @return $this */ public function setCreatedFromIp($createdFromIp) @@ -53,7 +70,8 @@ public function getCreatedFromIp() /** * Sets updatedFromIp. * - * @param string $updatedFromIp + * @param string $updatedFromIp + * * @return $this */ public function setUpdatedFromIp($updatedFromIp) diff --git a/src/Loggable/Document/LogEntry.php b/src/Loggable/Document/LogEntry.php new file mode 100644 index 0000000000..f4dce120ba --- /dev/null +++ b/src/Loggable/Document/LogEntry.php @@ -0,0 +1,41 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable\Document; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; +use Gedmo\Loggable\Document\MappedSuperclass\AbstractLogEntry; +use Gedmo\Loggable\Document\Repository\LogEntryRepository; +use Gedmo\Loggable\Loggable; + +/** + * Gedmo\Loggable\Document\LogEntry + * + * @MongoODM\Document(repositoryClass="Gedmo\Loggable\Document\Repository\LogEntryRepository") + * + * @MongoODM\Index(keys={"objectId": "asc", "objectClass": "asc", "version": "asc"}) + * @MongoODM\Index(keys={"loggedAt": "asc"}) + * @MongoODM\Index(keys={"objectClass": "asc"}) + * @MongoODM\Index(keys={"username": "asc"}) + * + * @phpstan-template T of Loggable|object + * + * @phpstan-extends AbstractLogEntry + */ +#[MongoODM\Document(repositoryClass: LogEntryRepository::class)] +#[MongoODM\Index(keys: ['objectId' => 'asc', 'objectClass' => 'asc', 'version' => 'asc'])] +#[MongoODM\Index(keys: ['loggedAt' => 'asc'])] +#[MongoODM\Index(keys: ['objectClass' => 'asc'])] +#[MongoODM\Index(keys: ['username' => 'asc'])] +class LogEntry extends AbstractLogEntry +{ + /* + * All required columns are mapped through inherited superclass + */ +} diff --git a/lib/Gedmo/Loggable/Document/MappedSuperclass/AbstractLogEntry.php b/src/Loggable/Document/MappedSuperclass/AbstractLogEntry.php similarity index 55% rename from lib/Gedmo/Loggable/Document/MappedSuperclass/AbstractLogEntry.php rename to src/Loggable/Document/MappedSuperclass/AbstractLogEntry.php index 7291f90c68..90f5514352 100644 --- a/lib/Gedmo/Loggable/Document/MappedSuperclass/AbstractLogEntry.php +++ b/src/Loggable/Document/MappedSuperclass/AbstractLogEntry.php @@ -1,79 +1,101 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Loggable\Document\MappedSuperclass; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; +use Doctrine\ODM\MongoDB\Types\Type; +use Gedmo\Loggable\LogEntryInterface; +use Gedmo\Loggable\Loggable; /** - * Gedmo\Loggable\Document\MappedSuperclass\AbstractLogEntry + * @phpstan-template T of Loggable|object + * + * @phpstan-implements LogEntryInterface * * @MongoODM\MappedSuperclass */ -abstract class AbstractLogEntry +#[MongoODM\MappedSuperclass] +abstract class AbstractLogEntry implements LogEntryInterface { /** - * @var integer $id + * @var string|null * * @MongoODM\Id */ + #[MongoODM\Id] protected $id; /** - * @var string $action + * @var string|null * - * @MongoODM\String + * @phpstan-var self::ACTION_CREATE|self::ACTION_UPDATE|self::ACTION_REMOVE|null + * + * @MongoODM\Field(type="string") */ + #[MongoODM\Field(type: Type::STRING)] protected $action; /** - * @var \DateTime $loggedAt + * @var \DateTime|null * - * @MongoODM\Index - * @MongoODM\Date + * @MongoODM\Field(type="date") */ + #[MongoODM\Field(type: Type::DATE)] protected $loggedAt; /** - * @var string $objectId + * @var string|null * - * @MongoODM\String(nullable=true) + * @MongoODM\Field(type="string", nullable=true) */ + #[MongoODM\Field(type: Type::STRING, nullable: true)] protected $objectId; /** - * @var string $objectClass + * @var string|null + * + * @phpstan-var class-string|null * - * @MongoODM\Index - * @MongoODM\String + * @MongoODM\Field(type="string") */ + #[MongoODM\Field(type: Type::STRING)] protected $objectClass; /** - * @var integer $version + * @var int|null * - * @MongoODM\Int + * @MongoODM\Field(type="int") */ + #[MongoODM\Field(type: Type::INT)] protected $version; /** - * @var string $data + * @var array|null * - * @MongoODM\Hash(nullable=true) + * @MongoODM\Field(type="hash", nullable=true) */ + #[MongoODM\Field(type: Type::HASH, nullable: true)] protected $data; /** - * @var string $data + * @var string|null * - * @MongoODM\Index - * @MongoODM\String(nullable=true) + * @MongoODM\Field(type="string", nullable=true) */ + #[MongoODM\Field(type: Type::STRING, nullable: true)] protected $username; /** * Get id * - * @return integer + * @return string|null */ public function getId() { @@ -83,7 +105,7 @@ public function getId() /** * Get action * - * @return string + * @return string|null */ public function getAction() { @@ -94,6 +116,8 @@ public function getAction() * Set action * * @param string $action + * + * @return void */ public function setAction($action) { @@ -103,7 +127,7 @@ public function setAction($action) /** * Get object class * - * @return string + * @return string|null */ public function getObjectClass() { @@ -114,6 +138,8 @@ public function getObjectClass() * Set object class * * @param string $objectClass + * + * @return void */ public function setObjectClass($objectClass) { @@ -123,7 +149,7 @@ public function setObjectClass($objectClass) /** * Get object id * - * @return string + * @return string|null */ public function getObjectId() { @@ -134,6 +160,8 @@ public function getObjectId() * Set object id * * @param string $objectId + * + * @return void */ public function setObjectId($objectId) { @@ -143,7 +171,7 @@ public function setObjectId($objectId) /** * Get username * - * @return string + * @return string|null */ public function getUsername() { @@ -154,6 +182,8 @@ public function getUsername() * Set username * * @param string $username + * + * @return void */ public function setUsername($username) { @@ -163,7 +193,7 @@ public function setUsername($username) /** * Get loggedAt * - * @return \DateTime + * @return \DateTime|null */ public function getLoggedAt() { @@ -172,6 +202,8 @@ public function getLoggedAt() /** * Set loggedAt to "now" + * + * @return void */ public function setLoggedAt() { @@ -181,7 +213,7 @@ public function setLoggedAt() /** * Get data * - * @return array or null + * @return array|null */ public function getData() { @@ -191,7 +223,9 @@ public function getData() /** * Set data * - * @param array $data + * @param array $data + * + * @return void */ public function setData($data) { @@ -201,7 +235,9 @@ public function setData($data) /** * Set current version * - * @param integer $version + * @param int $version + * + * @return void */ public function setVersion($version) { @@ -211,7 +247,7 @@ public function setVersion($version) /** * Get current version * - * @return integer + * @return int|null */ public function getVersion() { diff --git a/lib/Gedmo/Loggable/Document/Repository/LogEntryRepository.php b/src/Loggable/Document/Repository/LogEntryRepository.php similarity index 58% rename from lib/Gedmo/Loggable/Document/Repository/LogEntryRepository.php rename to src/Loggable/Document/Repository/LogEntryRepository.php index 7b6139d22e..0f52b5e770 100644 --- a/lib/Gedmo/Loggable/Document/Repository/LogEntryRepository.php +++ b/src/Loggable/Document/Repository/LogEntryRepository.php @@ -1,28 +1,41 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Loggable\Document\Repository; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\Repository\DocumentRepository; +use Gedmo\Exception\RuntimeException; +use Gedmo\Exception\UnexpectedValueException; use Gedmo\Loggable\Document\LogEntry; -use Gedmo\Tool\Wrapper\MongoDocumentWrapper; +use Gedmo\Loggable\Loggable; use Gedmo\Loggable\LoggableListener; -use Doctrine\ODM\MongoDB\DocumentRepository; -use Doctrine\ODM\MongoDB\Cursor; +use Gedmo\Tool\Wrapper\MongoDocumentWrapper; /** * The LogEntryRepository has some useful functions * to interact with log entries. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @phpstan-template T of Loggable|object + * + * @phpstan-extends DocumentRepository */ class LogEntryRepository extends DocumentRepository { /** * Currently used loggable listener * - * @var LoggableListener + * @var LoggableListener|null */ - private $listener; + private ?LoggableListener $listener = null; /** * Loads all log entries for the @@ -30,7 +43,11 @@ class LogEntryRepository extends DocumentRepository * * @param object $document * + * @phpstan-param T $document + * * @return LogEntry[] + * + * @phpstan-return array> */ public function getLogEntries($document) { @@ -39,15 +56,10 @@ public function getLogEntries($document) $qb = $this->createQueryBuilder(); $qb->field('objectId')->equals($objectId); - $qb->field('objectClass')->equals($wrapped->getMetadata()->name); + $qb->field('objectClass')->equals($wrapped->getMetadata()->getName()); $qb->sort('version', 'DESC'); - $q = $qb->getQuery(); - $result = $q->execute(); - if ($result instanceof Cursor) { - $result = $result->toArray(); - } - return $result; + return $qb->getQuery()->getIterator()->toArray(); } /** @@ -57,9 +69,11 @@ public function getLogEntries($document) * persist and flush the $document. * * @param object $document - * @param integer $version + * @param int $version * - * @throws \Gedmo\Exception\UnexpectedValueException + * @phpstan-param T $document + * + * @throws UnexpectedValueException * * @return void */ @@ -71,52 +85,61 @@ public function revert($document, $version = 1) $qb = $this->createQueryBuilder(); $qb->field('objectId')->equals($objectId); - $qb->field('objectClass')->equals($objectMeta->name); - $qb->field('version')->lte(intval($version)); + $qb->field('objectClass')->equals($objectMeta->getName()); + $qb->field('version')->lte((int) $version); $qb->sort('version', 'ASC'); - $q = $qb->getQuery(); - $logs = $q->execute(); - if ($logs instanceof Cursor) { - $logs = $logs->toArray(); + $logs = $qb->getQuery()->getIterator()->toArray(); + + if ([] === $logs) { + throw new UnexpectedValueException('Count not find any log entries under version: '.$version); } - if ($logs) { - $data = array(); - while (($log = array_shift($logs))) { - $data = array_merge($data, $log->getData()); - } - $this->fillDocument($document, $data, $objectMeta); - } else { - throw new \Gedmo\Exception\UnexpectedValueException('Count not find any log entries under version: '.$version); + + $data = [[]]; + while ($log = array_shift($logs)) { + $data[] = $log->getData(); } + $data = array_merge(...$data); + $this->fillDocument($document, $data); } /** * Fills a documents versioned fields with data * - * @param object $document - * @param array $data + * @param object $document + * @param array $data + * + * @phpstan-param T $document + * + * @return void */ protected function fillDocument($document, array $data) { $wrapped = new MongoDocumentWrapper($document, $this->dm); $objectMeta = $wrapped->getMetadata(); - $config = $this->getLoggableListener()->getConfiguration($this->dm, $objectMeta->name); + + assert($objectMeta instanceof ClassMetadata); + + $config = $this->getLoggableListener()->getConfiguration($this->dm, $objectMeta->getName()); $fields = $config['versioned']; foreach ($data as $field => $value) { - if (!in_array($field, $fields)) { + if (!in_array($field, $fields, true)) { continue; } $mapping = $objectMeta->getFieldMapping($field); // Fill the embedded document if ($wrapped->isEmbeddedAssociation($field)) { if (!empty($value)) { + assert(class_exists($mapping['targetDocument'])); + $embeddedMetadata = $this->dm->getClassMetadata($mapping['targetDocument']); $document = $embeddedMetadata->newInstance(); $this->fillDocument($document, $value); $value = $document; } } elseif ($objectMeta->isSingleValuedAssociation($field)) { + assert(class_exists($mapping['targetDocument'])); + $value = $value ? $this->dm->getReference($mapping['targetDocument'], $value) : null; } $wrapped->setPropertyValue($field, $value); @@ -133,29 +156,28 @@ protected function fillDocument($document, array $data) /** * Get the currently used LoggableListener * - * @throws \Gedmo\Exception\RuntimeException - if listener is not found + * @throws RuntimeException if listener is not found * - * @return LoggableListener + * @phpstan-return LoggableListener */ - private function getLoggableListener() + private function getLoggableListener(): LoggableListener { - if (is_null($this->listener)) { - foreach ($this->dm->getEventManager()->getListeners() as $event => $listeners) { - foreach ($listeners as $hash => $listener) { + if (null === $this->listener) { + foreach ($this->dm->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { if ($listener instanceof LoggableListener) { $this->listener = $listener; - break; + + break 2; } } - if ($this->listener) { - break; - } } - if (is_null($this->listener)) { - throw new \Gedmo\Exception\RuntimeException('The loggable listener could not be found'); + if (null === $this->listener) { + throw new RuntimeException('The loggable listener could not be found'); } } + return $this->listener; } } diff --git a/src/Loggable/Entity/LogEntry.php b/src/Loggable/Entity/LogEntry.php new file mode 100644 index 0000000000..ae2658aec2 --- /dev/null +++ b/src/Loggable/Entity/LogEntry.php @@ -0,0 +1,47 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry; +use Gedmo\Loggable\Entity\Repository\LogEntryRepository; +use Gedmo\Loggable\Loggable; + +/** + * Gedmo\Loggable\Entity\LogEntry + * + * @ORM\Table( + * name="ext_log_entries", + * options={"row_format": "DYNAMIC"}, + * indexes={ + * @ORM\Index(name="log_class_lookup_idx", columns={"object_class"}), + * @ORM\Index(name="log_date_lookup_idx", columns={"logged_at"}), + * @ORM\Index(name="log_user_lookup_idx", columns={"username"}), + * @ORM\Index(name="log_version_lookup_idx", columns={"object_id", "object_class", "version"}) + * } + * ) + * @ORM\Entity(repositoryClass="Gedmo\Loggable\Entity\Repository\LogEntryRepository") + * + * @phpstan-template T of Loggable|object + * + * @phpstan-extends AbstractLogEntry + */ +#[ORM\Entity(repositoryClass: LogEntryRepository::class)] +#[ORM\Table(name: 'ext_log_entries', options: ['row_format' => 'DYNAMIC'])] +#[ORM\Index(name: 'log_class_lookup_idx', columns: ['object_class'])] +#[ORM\Index(name: 'log_date_lookup_idx', columns: ['logged_at'])] +#[ORM\Index(name: 'log_user_lookup_idx', columns: ['username'])] +#[ORM\Index(name: 'log_version_lookup_idx', columns: ['object_id', 'object_class', 'version'])] +class LogEntry extends AbstractLogEntry +{ + /* + * All required columns are mapped through inherited superclass + */ +} diff --git a/lib/Gedmo/Loggable/Entity/MappedSuperclass/AbstractLogEntry.php b/src/Loggable/Entity/MappedSuperclass/AbstractLogEntry.php similarity index 62% rename from lib/Gedmo/Loggable/Entity/MappedSuperclass/AbstractLogEntry.php rename to src/Loggable/Entity/MappedSuperclass/AbstractLogEntry.php index e758e6799f..d0965547a4 100644 --- a/lib/Gedmo/Loggable/Entity/MappedSuperclass/AbstractLogEntry.php +++ b/src/Loggable/Entity/MappedSuperclass/AbstractLogEntry.php @@ -1,78 +1,107 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Loggable\Entity\MappedSuperclass; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\LogEntryInterface; +use Gedmo\Loggable\Loggable; /** - * Gedmo\Loggable\Entity\AbstractLog + * @phpstan-template T of Loggable|object + * + * @phpstan-implements LogEntryInterface * * @ORM\MappedSuperclass */ -abstract class AbstractLogEntry +#[ORM\MappedSuperclass] +abstract class AbstractLogEntry implements LogEntryInterface { /** - * @var integer $id + * @var int|null * * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue */ + #[ORM\Column(type: Types::INTEGER)] + #[ORM\Id] + #[ORM\GeneratedValue] protected $id; /** - * @var string $action + * @var string|null + * + * @phpstan-var self::ACTION_CREATE|self::ACTION_UPDATE|self::ACTION_REMOVE|null * * @ORM\Column(type="string", length=8) */ + #[ORM\Column(type: Types::STRING, length: 8)] protected $action; /** - * @var string $loggedAt + * @var \DateTime|null * * @ORM\Column(name="logged_at", type="datetime") */ + #[ORM\Column(name: 'logged_at', type: Types::DATETIME_MUTABLE)] protected $loggedAt; /** - * @var string $objectId + * @var string|null * * @ORM\Column(name="object_id", length=64, nullable=true) */ + #[ORM\Column(name: 'object_id', length: 64, nullable: true)] protected $objectId; /** - * @var string $objectClass + * @var string|null * - * @ORM\Column(name="object_class", type="string", length=255) + * @phpstan-var class-string|null + * + * @ORM\Column(name="object_class", type="string", length=191) */ + #[ORM\Column(name: 'object_class', type: Types::STRING, length: 191)] protected $objectClass; /** - * @var integer $version + * @var int|null * * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] protected $version; /** - * @var string $data + * @var array|null * * @ORM\Column(type="array", nullable=true) + * + * NOTE: The attribute uses the "array" name directly instead of the constant since it was removed in DBAL 4.0. */ + #[ORM\Column(type: 'array', nullable: true)] protected $data; /** - * @var string $data + * @var string|null * - * @ORM\Column(length=255, nullable=true) + * @ORM\Column(length=191, nullable=true) */ + #[ORM\Column(length: 191, nullable: true)] protected $username; /** * Get id * - * @return integer + * @return int|null */ public function getId() { @@ -81,8 +110,6 @@ public function getId() /** * Get action - * - * @return string */ public function getAction() { @@ -91,8 +118,6 @@ public function getAction() /** * Set action - * - * @param string $action */ public function setAction($action) { @@ -101,8 +126,6 @@ public function setAction($action) /** * Get object class - * - * @return string */ public function getObjectClass() { @@ -111,8 +134,6 @@ public function getObjectClass() /** * Set object class - * - * @param string $objectClass */ public function setObjectClass($objectClass) { @@ -121,8 +142,6 @@ public function setObjectClass($objectClass) /** * Get object id - * - * @return string */ public function getObjectId() { @@ -141,8 +160,6 @@ public function setObjectId($objectId) /** * Get username - * - * @return string */ public function getUsername() { @@ -161,8 +178,6 @@ public function setUsername($username) /** * Get loggedAt - * - * @return \DateTime */ public function getLoggedAt() { @@ -179,8 +194,6 @@ public function setLoggedAt() /** * Get data - * - * @return array */ public function getData() { @@ -189,8 +202,6 @@ public function getData() /** * Set data - * - * @param array $data */ public function setData($data) { @@ -200,7 +211,7 @@ public function setData($data) /** * Set current version * - * @param integer $version + * @param int $version */ public function setVersion($version) { @@ -209,8 +220,6 @@ public function setVersion($version) /** * Get current version - * - * @return integer */ public function getVersion() { diff --git a/src/Loggable/Entity/Repository/LogEntryRepository.php b/src/Loggable/Entity/Repository/LogEntryRepository.php new file mode 100644 index 0000000000..3b0dc9a3db --- /dev/null +++ b/src/Loggable/Entity/Repository/LogEntryRepository.php @@ -0,0 +1,189 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable\Entity\Repository; + +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query; +use Gedmo\Exception\RuntimeException; +use Gedmo\Exception\UnexpectedValueException; +use Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry; +use Gedmo\Loggable\Loggable; +use Gedmo\Loggable\LoggableListener; +use Gedmo\Tool\Wrapper\EntityWrapper; + +/** + * The LogEntryRepository has some useful functions + * to interact with log entries. + * + * @author Gediminas Morkevicius + * + * @template T of Loggable|object + * + * @template-extends EntityRepository> + */ +class LogEntryRepository extends EntityRepository +{ + /** + * Currently used loggable listener + * + * @var LoggableListener|null + */ + private ?LoggableListener $listener = null; + + /** + * Loads all log entries for the given entity + * + * @param T $entity + * + * @return array> + */ + public function getLogEntries($entity) + { + return $this->getLogEntriesQuery($entity)->getResult(); + } + + /** + * Get the query for loading of log entries + * + * @param T $entity + * + * @return Query + */ + public function getLogEntriesQuery($entity) + { + $wrapped = new EntityWrapper($entity, $this->getEntityManager()); + $objectClass = $wrapped->getMetadata()->getName(); + $meta = $this->getClassMetadata(); + $dql = "SELECT log FROM {$meta->getName()} log"; + $dql .= ' WHERE log.objectId = :objectId'; + $dql .= ' AND log.objectClass = :objectClass'; + $dql .= ' ORDER BY log.version DESC'; + + $objectId = (string) $wrapped->getIdentifier(false, true); + $q = $this->getEntityManager()->createQuery($dql); + $q->setParameters([ + 'objectId' => $objectId, + 'objectClass' => $objectClass, + ]); + + return $q; + } + + /** + * Reverts given $entity to $revision by + * restoring all fields from that $revision. + * After this operation you will need to + * persist and flush the $entity. + * + * @param T $entity + * @param int $version + * + * @throws UnexpectedValueException + * + * @return void + */ + public function revert($entity, $version = 1) + { + $wrapped = new EntityWrapper($entity, $this->getEntityManager()); + $objectMeta = $wrapped->getMetadata(); + $objectClass = $objectMeta->getName(); + $meta = $this->getClassMetadata(); + $dql = "SELECT log FROM {$meta->getName()} log"; + $dql .= ' WHERE log.objectId = :objectId'; + $dql .= ' AND log.objectClass = :objectClass'; + $dql .= ' AND log.version <= :version'; + $dql .= ' ORDER BY log.version DESC'; + + $objectId = (string) $wrapped->getIdentifier(false, true); + $q = $this->getEntityManager()->createQuery($dql); + $q->setParameters([ + 'objectId' => $objectId, + 'objectClass' => $objectClass, + 'version' => $version, + ]); + + $config = $this->getLoggableListener()->getConfiguration($this->getEntityManager(), $objectMeta->getName()); + $fields = $config['versioned']; + $filled = false; + $logsFound = false; + + $logs = $q->toIterable(); + assert($logs instanceof \Generator); + + while ((null !== $log = $logs->current()) && !$filled) { + $logsFound = true; + $logs->next(); + if ($data = $log->getData()) { + foreach ($data as $field => $value) { + if (in_array($field, $fields, true)) { + $this->mapValue($objectMeta, $field, $value); + $wrapped->setPropertyValue($field, $value); + unset($fields[array_search($field, $fields, true)]); + } + } + } + $filled = [] === $fields; + } + + if (!$logsFound) { + throw new UnexpectedValueException('Could not find any log entries under version: '.$version); + } + + /*if (count($fields)) { + throw new \Gedmo\Exception\UnexpectedValueException('Could not fully revert the entity to version: '.$version); + }*/ + } + + /** + * @param ClassMetadata $objectMeta + * @param string $field + * @param mixed $value + * + * @return void + */ + protected function mapValue(ClassMetadata $objectMeta, $field, &$value) + { + if (!$objectMeta->isSingleValuedAssociation($field)) { + return; + } + + $mapping = $objectMeta->getAssociationMapping($field); + $value = $value ? $this->getEntityManager()->getReference($mapping->targetEntity ?? $mapping['targetEntity'], $value) : null; + } + + /** + * Get the currently used LoggableListener + * + * @throws RuntimeException if listener is not found + * + * @return LoggableListener + */ + private function getLoggableListener(): LoggableListener + { + if (null === $this->listener) { + foreach ($this->getEntityManager()->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { + if ($listener instanceof LoggableListener) { + $this->listener = $listener; + + break 2; + } + } + } + + if (null === $this->listener) { + throw new RuntimeException('The loggable listener could not be found'); + } + } + + return $this->listener; + } +} diff --git a/src/Loggable/LogEntryInterface.php b/src/Loggable/LogEntryInterface.php new file mode 100644 index 0000000000..a283580ece --- /dev/null +++ b/src/Loggable/LogEntryInterface.php @@ -0,0 +1,106 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable; + +/** + * Interface to be implemented by log entry models. + * + * @phpstan-template T of Loggable|object + * + * @author Javier Spagnoletti + */ +interface LogEntryInterface +{ + public const ACTION_CREATE = 'create'; + + public const ACTION_UPDATE = 'update'; + + public const ACTION_REMOVE = 'remove'; + + /** + * @phpstan-param self::ACTION_CREATE|self::ACTION_UPDATE|self::ACTION_REMOVE $action + * + * @return void + */ + public function setAction(string $action); + + /** + * @return string|null + * + * @phpstan-return self::ACTION_CREATE|self::ACTION_UPDATE|self::ACTION_REMOVE|null + */ + public function getAction(); + + /** + * @return void + */ + public function setUsername(string $username); + + /** + * @return string|null + */ + public function getUsername(); + + /** + * @phpstan-param class-string $objectClass + * + * @return void + */ + public function setObjectClass(string $objectClass); + + /** + * @return string|null + * + * @phpstan-return class-string|null + */ + public function getObjectClass(); + + /** + * @return void + */ + public function setLoggedAt(); + + /** + * @return \DateTimeInterface|null + */ + public function getLoggedAt(); + + /** + * @return void + */ + public function setObjectId(string $objectId); + + /** + * @return string|null + */ + public function getObjectId(); + + /** + * @param array $data + * + * @return void + */ + public function setData(array $data); + + /** + * @return array|null + */ + public function getData(); + + /** + * @return void + */ + public function setVersion(int $version); + + /** + * @return int|null + */ + public function getVersion(); +} diff --git a/src/Loggable/Loggable.php b/src/Loggable/Loggable.php new file mode 100644 index 0000000000..1a79aa3784 --- /dev/null +++ b/src/Loggable/Loggable.php @@ -0,0 +1,32 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable; + +/** + * Marker interface for objects which can be identified as loggable. + * + * @author Gediminas Morkevicius + */ +interface Loggable +{ + // this interface is not necessary to implement + + /* + * @Gedmo\Loggable + * to mark the class as loggable use class annotation @Gedmo\Loggable + * this object will contain now a history + * available options: + * logEntryClass="My\LogEntryObject" (optional) defaultly will use internal object class + * example: + * + * @Gedmo\Loggable(logEntryClass="My\LogEntryObject") + * class MyEntity + */ +} diff --git a/src/Loggable/LoggableListener.php b/src/Loggable/LoggableListener.php new file mode 100644 index 0000000000..2c7836be9d --- /dev/null +++ b/src/Loggable/LoggableListener.php @@ -0,0 +1,421 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable; + +use Doctrine\Common\EventArgs; +use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata; +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Event\ManagerEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Exception\UnexpectedValueException; +use Gedmo\Loggable\Mapping\Event\LoggableAdapter; +use Gedmo\Mapping\MappedEventSubscriber; +use Gedmo\Tool\ActorProviderInterface; +use Gedmo\Tool\Wrapper\AbstractWrapper; + +/** + * Loggable listener + * + * @author Boussekeyt Jules + * @author Gediminas Morkevicius + * + * @phpstan-type LoggableConfiguration = array{ + * loggable?: bool, + * logEntryClass?: class-string>, + * useObjectClass?: class-string, + * versioned?: string[], + * } + * + * @template T of Loggable|object + * + * @phpstan-extends MappedEventSubscriber + */ +class LoggableListener extends MappedEventSubscriber +{ + /** + * @deprecated use `LogEntryInterface::ACTION_CREATE` instead + */ + public const ACTION_CREATE = LogEntryInterface::ACTION_CREATE; + + /** + * @deprecated use `LogEntryInterface::ACTION_UPDATE` instead + */ + public const ACTION_UPDATE = LogEntryInterface::ACTION_UPDATE; + + /** + * @deprecated use `LogEntryInterface::ACTION_REMOVE` instead + */ + public const ACTION_REMOVE = LogEntryInterface::ACTION_REMOVE; + + protected ?ActorProviderInterface $actorProvider = null; + + /** + * Username for identification + * + * @var string + */ + protected $username; + + /** + * List of log entries which do not have the foreign + * key generated yet - MySQL case. These entries + * will be updated with new keys on postPersist event + * + * @var array + * + * @phpstan-var array> + */ + protected $pendingLogEntryInserts = []; + + /** + * For log of changed relations we use + * its identifiers to avoid storing serialized Proxies. + * These are pending relations in case it does not + * have an identifier yet + * + * @var array>> + * + * @phpstan-var array, field: string}>> + */ + protected $pendingRelatedObjects = []; + + /** + * Set an actor provider for the user value. + */ + public function setActorProvider(ActorProviderInterface $actorProvider): void + { + $this->actorProvider = $actorProvider; + } + + /** + * Set username for identification + * + * If an actor provider is also provided, it will take precedence over this value. + * + * @param mixed $username + * + * @throws InvalidArgumentException Invalid username + * + * @return void + */ + public function setUsername($username) + { + if (is_string($username)) { + $this->username = $username; + } elseif (is_object($username) && method_exists($username, 'getUserIdentifier')) { + $this->username = (string) $username->getUserIdentifier(); + } elseif (is_object($username) && method_exists($username, 'getUsername')) { + $this->username = (string) $username->getUsername(); + } elseif (is_object($username) && method_exists($username, '__toString')) { + $this->username = $username->__toString(); + } else { + throw new InvalidArgumentException('Username must be a string, or object should have method getUserIdentifier, getUsername or __toString'); + } + } + + /** + * @return string[] + */ + public function getSubscribedEvents() + { + return [ + 'onFlush', + 'loadClassMetadata', + 'postPersist', + ]; + } + + /** + * Maps additional metadata + * + * @param LoadClassMetadataEventArgs $eventArgs + * + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $eventArgs + * + * @return void + */ + public function loadClassMetadata(EventArgs $eventArgs) + { + $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); + } + + /** + * Checks for inserted object to update its logEntry + * foreign key + * + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void + */ + public function postPersist(EventArgs $args) + { + $ea = $this->getEventAdapter($args); + $object = $ea->getObject(); + $om = $ea->getObjectManager(); + $oid = spl_object_id($object); + $uow = $om->getUnitOfWork(); + if ($this->pendingLogEntryInserts && array_key_exists($oid, $this->pendingLogEntryInserts)) { + $wrapped = AbstractWrapper::wrap($object, $om); + + $logEntry = $this->pendingLogEntryInserts[$oid]; + $logEntryMeta = $om->getClassMetadata(get_class($logEntry)); + + $id = $wrapped->getIdentifier(false, true); + $logEntryMeta->setFieldValue($logEntry, 'objectId', $id); + $uow->scheduleExtraUpdate($logEntry, [ + 'objectId' => [null, $id], + ]); + $ea->setOriginalObjectProperty($uow, $logEntry, 'objectId', $id); + unset($this->pendingLogEntryInserts[$oid]); + } + if ($this->pendingRelatedObjects && array_key_exists($oid, $this->pendingRelatedObjects)) { + $wrapped = AbstractWrapper::wrap($object, $om); + $identifiers = $wrapped->getIdentifier(false); + foreach ($this->pendingRelatedObjects[$oid] as $props) { + $logEntry = $props['log']; + $logEntryMeta = $om->getClassMetadata(get_class($logEntry)); + $oldData = $data = $logEntry->getData(); + $data[$props['field']] = $identifiers; + + $logEntry->setData($data); + + $uow->scheduleExtraUpdate($logEntry, [ + 'data' => [$oldData, $data], + ]); + $ea->setOriginalObjectProperty($uow, $logEntry, 'data', $data); + } + unset($this->pendingRelatedObjects[$oid]); + } + } + + /** + * Looks for loggable objects being inserted or updated + * for further processing + * + * @param ManagerEventArgs $eventArgs + * + * @phpstan-param ManagerEventArgs $eventArgs + * + * @return void + */ + public function onFlush(EventArgs $eventArgs) + { + $ea = $this->getEventAdapter($eventArgs); + $om = $ea->getObjectManager(); + $uow = $om->getUnitOfWork(); + + foreach ($ea->getScheduledObjectInsertions($uow) as $object) { + $this->createLogEntry(LogEntryInterface::ACTION_CREATE, $object, $ea); + } + foreach ($ea->getScheduledObjectUpdates($uow) as $object) { + $this->createLogEntry(LogEntryInterface::ACTION_UPDATE, $object, $ea); + } + foreach ($ea->getScheduledObjectDeletions($uow) as $object) { + $this->createLogEntry(LogEntryInterface::ACTION_REMOVE, $object, $ea); + } + } + + /** + * Get the LogEntry class + * + * @param string $class + * + * @phpstan-param class-string $class + * + * @return string + * + * @phpstan-return class-string> + */ + protected function getLogEntryClass(LoggableAdapter $ea, $class) + { + return self::$configurations[$this->name][$class]['logEntryClass'] ?? $ea->getDefaultLogEntryClass(); + } + + /** + * Retrieve the username to use for the log entry. + * + * This method will try to fetch a username from the actor provider first, falling back to the {@see $this->username} + * property if the provider is not set or does not provide a value. + * + * @throws UnexpectedValueException if the actor provider provides an unsupported username value + */ + protected function getUsername(): ?string + { + if ($this->actorProvider instanceof ActorProviderInterface) { + $actor = $this->actorProvider->getActor(); + + if (is_string($actor) || null === $actor) { + return $actor; + } + + if (method_exists($actor, 'getUserIdentifier')) { + return (string) $actor->getUserIdentifier(); + } + + if (method_exists($actor, 'getUsername')) { + return (string) $actor->getUsername(); + } + + if (method_exists($actor, '__toString')) { + return $actor->__toString(); + } + + throw new UnexpectedValueException(\sprintf('The loggable extension requires the actor provider to return a string or an object implementing the "getUserIdentifier()", "getUsername()", or "__toString()" methods. "%s" cannot be used as an actor.', get_class($actor))); + } + + return $this->username; + } + + /** + * Handle any custom LogEntry functionality that needs to be performed + * before persisting it + * + * @param LogEntryInterface $logEntry The LogEntry being persisted + * @param object $object The object being Logged + * + * @phpstan-param LogEntryInterface $logEntry + * @phpstan-param T $object + * + * @return void + */ + protected function prePersistLogEntry($logEntry, $object) + { + } + + protected function getNamespace() + { + return __NAMESPACE__; + } + + /** + * Returns an objects changeset data + * + * @param LoggableAdapter $ea + * @param object $object + * @param LogEntryInterface $logEntry + * + * @phpstan-param T $object + * @phpstan-param LogEntryInterface $logEntry + * + * @return array + */ + protected function getObjectChangeSetData($ea, $object, $logEntry) + { + $om = $ea->getObjectManager(); + $wrapped = AbstractWrapper::wrap($object, $om); + $meta = $wrapped->getMetadata(); + $config = $this->getConfiguration($om, $meta->getName()); + $uow = $om->getUnitOfWork(); + $newValues = []; + + foreach ($ea->getObjectChangeSet($uow, $object) as $field => $changes) { + if (empty($config['versioned']) || !in_array($field, $config['versioned'], true)) { + continue; + } + $value = $changes[1]; + if ($meta->isSingleValuedAssociation($field) && $value) { + if ($wrapped->isEmbeddedAssociation($field)) { + $value = $this->getObjectChangeSetData($ea, $value, $logEntry); + } else { + $oid = spl_object_id($value); + $wrappedAssoc = AbstractWrapper::wrap($value, $om); + $value = $wrappedAssoc->getIdentifier(false); + if (!is_array($value) && !$value) { + $this->pendingRelatedObjects[$oid][] = [ + 'log' => $logEntry, + 'field' => $field, + ]; + } + } + } + $newValues[$field] = $value; + } + + return $newValues; + } + + /** + * Create a new Log instance + * + * @param string $action + * @param object $object + * + * @phpstan-param LogEntryInterface::ACTION_CREATE|LogEntryInterface::ACTION_UPDATE|LogEntryInterface::ACTION_REMOVE $action + * @phpstan-param T $object + * + * @return LogEntryInterface|null + * + * @phpstan-return LogEntryInterface|null + */ + protected function createLogEntry($action, $object, LoggableAdapter $ea) + { + $om = $ea->getObjectManager(); + $wrapped = AbstractWrapper::wrap($object, $om); + $meta = $wrapped->getMetadata(); + + // Filter embedded documents + if (isset($meta->isEmbeddedDocument) && $meta->isEmbeddedDocument) { + return null; + } + + if ($config = $this->getConfiguration($om, $meta->getName())) { + $logEntryClass = $this->getLogEntryClass($ea, $meta->getName()); + $logEntryMeta = $om->getClassMetadata($logEntryClass); + /** @var LogEntryInterface $logEntry */ + $logEntry = $logEntryMeta->newInstance(); + + $logEntry->setAction($action); + $logEntry->setUsername($this->getUsername()); + $logEntry->setObjectClass($meta->getName()); + $logEntry->setLoggedAt(); + + // check for the availability of the primary key + $uow = $om->getUnitOfWork(); + if (LogEntryInterface::ACTION_CREATE === $action && ($ea->isPostInsertGenerator($meta) || ($meta instanceof ORMClassMetadata && $meta->isIdentifierComposite))) { + $this->pendingLogEntryInserts[spl_object_id($object)] = $logEntry; + } else { + $logEntry->setObjectId($wrapped->getIdentifier(false, true)); + } + $newValues = []; + if (LogEntryInterface::ACTION_REMOVE !== $action && isset($config['versioned'])) { + $newValues = $this->getObjectChangeSetData($ea, $object, $logEntry); + $logEntry->setData($newValues); + } + + if (LogEntryInterface::ACTION_UPDATE === $action && [] === $newValues) { + return null; + } + + $version = 1; + if (LogEntryInterface::ACTION_CREATE !== $action) { + $version = $ea->getNewVersion($logEntryMeta, $object); + if (empty($version)) { + // was versioned later + $version = 1; + } + } + $logEntry->setVersion($version); + + $this->prePersistLogEntry($logEntry, $object); + + $om->persist($logEntry); + $uow->computeChangeSet($logEntryMeta, $logEntry); + + return $logEntry; + } + + return null; + } +} diff --git a/src/Loggable/Mapping/Driver/Annotation.php b/src/Loggable/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..45f895952a --- /dev/null +++ b/src/Loggable/Mapping/Driver/Annotation.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the loggable extension which reads extended metadata from annotations on a loggable class. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/src/Loggable/Mapping/Driver/Attribute.php b/src/Loggable/Mapping/Driver/Attribute.php new file mode 100644 index 0000000000..84bf7d391b --- /dev/null +++ b/src/Loggable/Mapping/Driver/Attribute.php @@ -0,0 +1,158 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable\Mapping\Driver; + +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ClassMetadataODM; +use Doctrine\ORM\Mapping\ClassMetadata as ClassMetadataORM; +use Doctrine\ORM\Mapping\EmbeddedClassMapping; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Loggable; +use Gedmo\Mapping\Annotation\Versioned; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; + +/** + * Mapping driver for the loggable extension which reads extended metadata from attributes on a loggable class. + * + * @author Boussekeyt Jules + * @author Gediminas Morkevicius + * + * @internal + */ +class Attribute extends AbstractAnnotationDriver +{ + /** + * Mapping object defining a loggable class. + */ + public const LOGGABLE = Loggable::class; + + /** + * Mapping object defining a versioned property from a loggable class. + */ + public const VERSIONED = Versioned::class; + + public function validateFullMetadata(ClassMetadata $meta, array $config) + { + if ($config && $meta instanceof ClassMetadataODM && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->getName()}"); + } + + if (isset($config['versioned']) && !isset($config['loggable'])) { + throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->getName()}"); + } + } + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // class annotations + if ($annot = $this->reader->getClassAnnotation($class, self::LOGGABLE)) { + \assert($annot instanceof Loggable); + + $config['loggable'] = true; + + if ($annot->logEntryClass) { + if (!$cl = $this->getRelatedClassName($meta, $annot->logEntryClass)) { + throw new InvalidMappingException("LogEntry class: {$annot->logEntryClass} does not exist."); + } + + $config['logEntryClass'] = $cl; + } + } + + // property annotations + foreach ($class->getProperties() as $property) { + $field = $property->getName(); + + if ($meta->isMappedSuperclass && !$property->isPrivate()) { + continue; + } + + // versioned property + if ($this->reader->getPropertyAnnotation($property, self::VERSIONED)) { + if (!$this->isMappingValid($meta, $field)) { + throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); + } + + if (isset($meta->embeddedClasses[$field])) { + $this->inspectEmbeddedForVersioned($field, $config, $meta); + + continue; + } + + // fields cannot be overridden and throws mapping exception + if (!in_array($field, $config['versioned'] ?? [], true)) { + $config['versioned'][] = $field; + } + } + } + + if (!$meta->isMappedSuperclass && $config) { + if ($meta instanceof ClassMetadataODM && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->getName()}"); + } + + if ($this->isClassAnnotationInValid($meta, $config)) { + throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->getName()}"); + } + } + + return $config; + } + + /** + * @param ClassMetadata $meta + * @param string $field + * + * @return bool + */ + protected function isMappingValid(ClassMetadata $meta, $field) + { + return false == $meta->isCollectionValuedAssociation($field); + } + + /** + * @param ClassMetadata $meta + * @param array $config + * + * @return bool + */ + protected function isClassAnnotationInValid(ClassMetadata $meta, array &$config) + { + return isset($config['versioned']) && !isset($config['loggable']) && (!isset($meta->isEmbeddedClass) || !$meta->isEmbeddedClass); + } + + /** + * Searches properties of embedded objects for versioned fields + * + * @param array $config + * @param ClassMetadataORM $meta + */ + private function inspectEmbeddedForVersioned(string $field, array &$config, ClassMetadataORM $meta): void + { + /** Remove conditional when ORM 2.x is no longer supported. */ + $className = ($meta->embeddedClasses[$field] instanceof EmbeddedClassMapping) ? $meta->embeddedClasses[$field]->class : $meta->embeddedClasses[$field]['class']; + $class = new \ReflectionClass($className); + + // property annotations + foreach ($class->getProperties() as $property) { + // versioned property + if ($this->reader->getPropertyAnnotation($property, self::VERSIONED)) { + $embeddedField = $field.'.'.$property->getName(); + $config['versioned'][] = $embeddedField; + + if (isset($meta->embeddedClasses[$embeddedField])) { + $this->inspectEmbeddedForVersioned($embeddedField, $config, $meta); + } + } + } + } +} diff --git a/lib/Gedmo/Loggable/Mapping/Driver/Xml.php b/src/Loggable/Mapping/Driver/Xml.php similarity index 60% rename from lib/Gedmo/Loggable/Mapping/Driver/Xml.php rename to src/Loggable/Mapping/Driver/Xml.php index 9a3f3b275e..cd00c02da9 100644 --- a/lib/Gedmo/Loggable/Mapping/Driver/Xml.php +++ b/src/Loggable/Mapping/Driver/Xml.php @@ -1,9 +1,18 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Loggable\Mapping\Driver; -use Gedmo\Mapping\Driver\Xml as BaseXml; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ClassMetadataODM; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for Loggable @@ -14,27 +23,25 @@ * @author Boussekeyt Jules * @author Gediminas Morkevicius * @author Miha Vrhovnik - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ class Xml extends BaseXml { - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { /** - * @var \SimpleXmlElement $xml + * @var \SimpleXmlElement */ - $xml = $this->_getMapping($meta->name); + $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); - if ($xmlDoctrine->getName() == 'entity' || $xmlDoctrine->getName() == 'document' || $xmlDoctrine->getName() == 'mapped-superclass') { + if (in_array($xmlDoctrine->getName(), ['mapped-superclass', 'entity', 'document'], true)) { if (isset($xml->loggable)) { /** - * @var \SimpleXMLElement $data; + * @var \SimpleXMLElement */ $data = $xml->loggable; $config['loggable'] = true; @@ -49,44 +56,50 @@ public function readExtendedMetadata($meta, array &$config) } if (isset($xmlDoctrine->field)) { - $this->inspectElementForVersioned($xmlDoctrine->field, $config, $meta); + $config = $this->inspectElementForVersioned($xmlDoctrine->field, $config, $meta); + } + foreach ($xmlDoctrine->{'attribute-overrides'}->{'attribute-override'} ?? [] as $overrideMapping) { + $config = $this->inspectElementForVersioned($overrideMapping, $config, $meta); } if (isset($xmlDoctrine->{'many-to-one'})) { - $this->inspectElementForVersioned($xmlDoctrine->{'many-to-one'}, $config, $meta); + $config = $this->inspectElementForVersioned($xmlDoctrine->{'many-to-one'}, $config, $meta); } if (isset($xmlDoctrine->{'one-to-one'})) { - $this->inspectElementForVersioned($xmlDoctrine->{'one-to-one'}, $config, $meta); + $config = $this->inspectElementForVersioned($xmlDoctrine->{'one-to-one'}, $config, $meta); } if (isset($xmlDoctrine->{'reference-one'})) { - $this->inspectElementForVersioned($xmlDoctrine->{'reference-one'}, $config, $meta); + $config = $this->inspectElementForVersioned($xmlDoctrine->{'reference-one'}, $config, $meta); } if (isset($xmlDoctrine->{'embedded'})) { - $this->inspectElementForVersioned($xmlDoctrine->{'embedded'}, $config, $meta); + $config = $this->inspectElementForVersioned($xmlDoctrine->{'embedded'}, $config, $meta); } if (!$meta->isMappedSuperclass && $config) { - if (is_array($meta->identifier) && count($meta->identifier) > 1) { - throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->name}"); + if ($meta instanceof ClassMetadataODM && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->getName()}"); } if (isset($config['versioned']) && !isset($config['loggable'])) { - throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->name}"); + throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->getName()}"); } } + + return $config; } /** * Searches mappings on element for versioned fields * - * @param \SimpleXMLElement $element - * @param array $config - * @param object $meta + * @param array $config + * @param ClassMetadata $meta + * + * @return array */ - private function inspectElementForVersioned(\SimpleXMLElement $element, array &$config, $meta) + private function inspectElementForVersioned(\SimpleXMLElement $element, array $config, ClassMetadata $meta): array { foreach ($element as $mapping) { $mappingDoctrine = $mapping; /** - * @var \SimpleXmlElement $mapping + * @var \SimpleXmlElement */ $mapping = $mapping->children(self::GEDMO_NAMESPACE_URI); @@ -95,10 +108,12 @@ private function inspectElementForVersioned(\SimpleXMLElement $element, array &$ if (isset($mapping->versioned)) { if ($isAssoc && !$meta->associationMappings[$field]['isOwningSide']) { - throw new InvalidMappingException("Cannot version [{$field}] as it is not the owning side in object - {$meta->name}"); + throw new InvalidMappingException("Cannot version [{$field}] as it is not the owning side in object - {$meta->getName()}"); } $config['versioned'][] = $field; } } + + return $config; } } diff --git a/lib/Gedmo/Loggable/Mapping/Driver/Yaml.php b/src/Loggable/Mapping/Driver/Yaml.php similarity index 60% rename from lib/Gedmo/Loggable/Mapping/Driver/Yaml.php rename to src/Loggable/Mapping/Driver/Yaml.php index 42a2949d76..7863059de6 100644 --- a/lib/Gedmo/Loggable/Mapping/Driver/Yaml.php +++ b/src/Loggable/Mapping/Driver/Yaml.php @@ -1,10 +1,18 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Loggable\Mapping\Driver; -use Gedmo\Mapping\Driver\File; -use Gedmo\Mapping\Driver; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Loggable @@ -14,28 +22,29 @@ * * @author Boussekeyt Jules * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ -class Yaml extends File implements Driver +class Yaml extends File { /** * File extension + * * @var string */ protected $_extension = '.dcm.yml'; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['gedmo'])) { $classMapping = $mapping['gedmo']; if (isset($classMapping['loggable'])) { $config['loggable'] = true; - if (isset ($classMapping['loggable']['logEntryClass'])) { + if (isset($classMapping['loggable']['logEntryClass'])) { if (!$cl = $this->getRelatedClassName($meta, $classMapping['loggable']['logEntryClass'])) { throw new InvalidMappingException("LogEntry class: {$classMapping['loggable']['logEntryClass']} does not exist."); } @@ -47,9 +56,9 @@ public function readExtendedMetadata($meta, array &$config) if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { - if (in_array('versioned', $fieldMapping['gedmo'])) { + if (in_array('versioned', $fieldMapping['gedmo'], true)) { if ($meta->isCollectionValuedAssociation($field)) { - throw new InvalidMappingException("Cannot versioned [{$field}] as it is collection in object - {$meta->name}"); + throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); } // fields cannot be overrided and throws mapping exception $config['versioned'][] = $field; @@ -61,9 +70,9 @@ public function readExtendedMetadata($meta, array &$config) if (isset($mapping['attributeOverride'])) { foreach ($mapping['attributeOverride'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { - if (in_array('versioned', $fieldMapping['gedmo'])) { + if (in_array('versioned', $fieldMapping['gedmo'], true)) { if ($meta->isCollectionValuedAssociation($field)) { - throw new InvalidMappingException("Cannot versioned [{$field}] as it is collection in object - {$meta->name}"); + throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); } // fields cannot be overrided and throws mapping exception $config['versioned'][] = $field; @@ -75,9 +84,9 @@ public function readExtendedMetadata($meta, array &$config) if (isset($mapping['manyToOne'])) { foreach ($mapping['manyToOne'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { - if (in_array('versioned', $fieldMapping['gedmo'])) { + if (in_array('versioned', $fieldMapping['gedmo'], true)) { if ($meta->isCollectionValuedAssociation($field)) { - throw new InvalidMappingException("Cannot versioned [{$field}] as it is collection in object - {$meta->name}"); + throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); } // fields cannot be overrided and throws mapping exception $config['versioned'][] = $field; @@ -89,9 +98,9 @@ public function readExtendedMetadata($meta, array &$config) if (isset($mapping['oneToOne'])) { foreach ($mapping['oneToOne'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { - if (in_array('versioned', $fieldMapping['gedmo'])) { + if (in_array('versioned', $fieldMapping['gedmo'], true)) { if ($meta->isCollectionValuedAssociation($field)) { - throw new InvalidMappingException("Cannot versioned [{$field}] as it is collection in object - {$meta->name}"); + throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); } // fields cannot be overrided and throws mapping exception $config['versioned'][] = $field; @@ -100,21 +109,52 @@ public function readExtendedMetadata($meta, array &$config) } } + if (isset($mapping['embedded'])) { + foreach ($mapping['embedded'] as $field => $fieldMapping) { + if (isset($fieldMapping['gedmo'])) { + if (in_array('versioned', $fieldMapping['gedmo'], true)) { + if ($meta->isCollectionValuedAssociation($field)) { + throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); + } + // fields cannot be overrided and throws mapping exception + $mapping = $this->_getMapping($fieldMapping['class']); + $config = $this->inspectEmbeddedForVersioned($field, $mapping, $config); + } + } + } + } + if (!$meta->isMappedSuperclass && $config) { - if (is_array($meta->identifier) && count($meta->identifier) > 1) { - throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->name}"); + if ($meta instanceof ClassMetadata && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->getName()}"); } if (isset($config['versioned']) && !isset($config['loggable'])) { - throw new InvalidMappingException("Class must be annoted with Loggable annotation in order to track versioned fields in class - {$meta->name}"); + throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->getName()}"); } } + + return $config; } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } + + /** + * @param array>> $mapping + * @param array $config + * + * @return array + */ + private function inspectEmbeddedForVersioned(string $field, array $mapping, array $config): array + { + if (isset($mapping['fields'])) { + foreach ($mapping['fields'] as $property => $fieldMapping) { + $config['versioned'][] = $field.'.'.$property; + } + } + + return $config; + } } diff --git a/lib/Gedmo/Loggable/Mapping/Event/Adapter/ODM.php b/src/Loggable/Mapping/Event/Adapter/ODM.php similarity index 62% rename from lib/Gedmo/Loggable/Mapping/Event/Adapter/ODM.php rename to src/Loggable/Mapping/Event/Adapter/ODM.php index 0aa5df0096..281ee1766c 100644 --- a/lib/Gedmo/Loggable/Mapping/Event/Adapter/ODM.php +++ b/src/Loggable/Mapping/Event/Adapter/ODM.php @@ -1,29 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Loggable\Mapping\Event\Adapter; -use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Gedmo\Loggable\Document\LogEntry; use Gedmo\Loggable\Mapping\Event\LoggableAdapter; +use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; /** * Doctrine event adapter for ODM adapted * for Loggable behavior * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ODM extends BaseAdapterODM implements LoggableAdapter { - /** - * {@inheritDoc} - */ public function getDefaultLogEntryClass() { - return 'Gedmo\\Loggable\\Document\\LogEntry'; + return LogEntry::class; } /** - * {@inheritDoc} + * @param ClassMetadata $meta */ public function isPostInsertGenerator($meta) { @@ -31,19 +36,19 @@ public function isPostInsertGenerator($meta) } /** - * {@inheritDoc} + * @param ClassMetadata $meta */ public function getNewVersion($meta, $object) { $dm = $this->getObjectManager(); $objectMeta = $dm->getClassMetadata(get_class($object)); $identifierField = $this->getSingleIdentifierFieldName($objectMeta); - $objectId = $objectMeta->getReflectionProperty($identifierField)->getValue($object); + $objectId = $objectMeta->getFieldValue($object, $identifierField); - $qb = $dm->createQueryBuilder($meta->name); + $qb = $dm->createQueryBuilder($meta->getName()); $qb->select('version'); $qb->field('objectId')->equals($objectId); - $qb->field('objectClass')->equals($objectMeta->name); + $qb->field('objectClass')->equals($objectMeta->getName()); $qb->sort('version', 'DESC'); $qb->limit(1); $q = $qb->getQuery(); diff --git a/src/Loggable/Mapping/Event/Adapter/ORM.php b/src/Loggable/Mapping/Event/Adapter/ORM.php new file mode 100644 index 0000000000..83fcac8ad4 --- /dev/null +++ b/src/Loggable/Mapping/Event/Adapter/ORM.php @@ -0,0 +1,61 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable\Mapping\Event\Adapter; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Gedmo\Loggable\Entity\LogEntry; +use Gedmo\Loggable\Mapping\Event\LoggableAdapter; +use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; +use Gedmo\Tool\Wrapper\EntityWrapper; + +/** + * Doctrine event adapter for ORM adapted + * for Loggable behavior + * + * @author Gediminas Morkevicius + */ +final class ORM extends BaseAdapterORM implements LoggableAdapter +{ + public function getDefaultLogEntryClass() + { + return LogEntry::class; + } + + /** + * @param ClassMetadata $meta + */ + public function isPostInsertGenerator($meta) + { + return $meta->idGenerator->isPostInsertGenerator(); + } + + /** + * @param ClassMetadata $meta + */ + public function getNewVersion($meta, $object) + { + $em = $this->getObjectManager(); + $objectMeta = $em->getClassMetadata(get_class($object)); + $wrapper = new EntityWrapper($object, $em); + $objectId = $wrapper->getIdentifier(false, true); + + $dql = "SELECT MAX(log.version) FROM {$meta->getName()} log"; + $dql .= ' WHERE log.objectId = :objectId'; + $dql .= ' AND log.objectClass = :objectClass'; + + $q = $em->createQuery($dql); + $q->setParameters([ + 'objectId' => $objectId, + 'objectClass' => $objectMeta->getName(), + ]); + + return $q->getSingleScalarResult() + 1; + } +} diff --git a/src/Loggable/Mapping/Event/LoggableAdapter.php b/src/Loggable/Mapping/Event/LoggableAdapter.php new file mode 100644 index 0000000000..bc960d72bb --- /dev/null +++ b/src/Loggable/Mapping/Event/LoggableAdapter.php @@ -0,0 +1,49 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable\Mapping\Event; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Mapping\Event\AdapterInterface; + +/** + * Doctrine event adapter for the Loggable extension. + * + * @author Gediminas Morkevicius + */ +interface LoggableAdapter extends AdapterInterface +{ + /** + * Get the default object class name used to store the log entries. + * + * @return string + * + * @phpstan-return class-string + */ + public function getDefaultLogEntryClass(); + + /** + * Checks whether an identifier should be generated post insert. + * + * @param ClassMetadata $meta + * + * @return bool + */ + public function isPostInsertGenerator($meta); + + /** + * Get the new version number for an object. + * + * @param ClassMetadata $meta + * @param object $object + * + * @return int + */ + public function getNewVersion($meta, $object); +} diff --git a/src/Mapping/Annotation/All.php b/src/Mapping/Annotation/All.php new file mode 100644 index 0000000000..7f973f4c59 --- /dev/null +++ b/src/Mapping/Annotation/All.php @@ -0,0 +1,26 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Doctrine\Deprecations\Deprecation; + +Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2558', + 'Requiring the file at "%s" is deprecated since gedmo/doctrine-extensions 3.11, this file will be removed in version 4.0.', + __FILE__ +); + +// Contains all annotations for extensions +// NOTE: should be included with require_once +foreach (glob(__DIR__.'/*.php') as $filename) { + if ('All' === basename($filename, '.php')) { + continue; + } + include_once $filename; +} diff --git a/src/Mapping/Annotation/Annotation.php b/src/Mapping/Annotation/Annotation.php new file mode 100644 index 0000000000..85a5f412e8 --- /dev/null +++ b/src/Mapping/Annotation/Annotation.php @@ -0,0 +1,19 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +/** + * @internal + */ +interface Annotation +{ +} diff --git a/src/Mapping/Annotation/Blameable.php b/src/Mapping/Annotation/Blameable.php new file mode 100644 index 0000000000..804adb819e --- /dev/null +++ b/src/Mapping/Annotation/Blameable.php @@ -0,0 +1,66 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Blameable annotation for Blameable behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("PROPERTY") + * + * @author David Buchmann + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class Blameable implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + public string $on = 'update'; + /** @var string|string[] */ + public $field; + /** @var mixed */ + public $value; + + /** + * @param array $data + * @param string|string[]|null $field + * @param mixed $value + */ + public function __construct(array $data = [], string $on = 'update', $field = null, $value = null) + { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2375', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->on = $this->getAttributeValue($data, 'on', $args, 1, $on); + $this->field = $this->getAttributeValue($data, 'field', $args, 2, $field); + $this->value = $this->getAttributeValue($data, 'value', $args, 3, $value); + + return; + } + + $this->on = $on; + $this->field = $field; + $this->value = $value; + } +} diff --git a/src/Mapping/Annotation/ForwardCompatibilityTrait.php b/src/Mapping/Annotation/ForwardCompatibilityTrait.php new file mode 100644 index 0000000000..7fff690273 --- /dev/null +++ b/src/Mapping/Annotation/ForwardCompatibilityTrait.php @@ -0,0 +1,40 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +/** + * @todo Remove this trait when support for array based attributes is removed. + * + * @internal + */ +trait ForwardCompatibilityTrait +{ + /** + * @param array $data + * @param array $args + * @param mixed $value + * + * @return mixed + */ + private function getAttributeValue(array $data, string $attributeName, array $args, int $argumentNum, $value) + { + if (array_key_exists($argumentNum, $args)) { + return $args[$argumentNum]; + } + + if (array_key_exists($attributeName, $data)) { + return $data[$attributeName]; + } + + return $value; + } +} diff --git a/src/Mapping/Annotation/IpTraceable.php b/src/Mapping/Annotation/IpTraceable.php new file mode 100644 index 0000000000..a438f46b21 --- /dev/null +++ b/src/Mapping/Annotation/IpTraceable.php @@ -0,0 +1,67 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * IpTraceable annotation for IpTraceable behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("PROPERTY") + * + * @author Pierre-Charles Bertineau + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class IpTraceable implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** @var string */ + public $on = 'update'; + /** @var string|string[]|null */ + public $field; + /** @var mixed */ + public $value; + + /** + * @param array $data + * @param string|string[]|null $field + * @param mixed $value + */ + public function __construct(array $data = [], string $on = 'update', $field = null, $value = null) + { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2377', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->on = $this->getAttributeValue($data, 'on', $args, 1, $on); + $this->field = $this->getAttributeValue($data, 'field', $args, 2, $field); + $this->value = $this->getAttributeValue($data, 'value', $args, 3, $value); + + return; + } + + $this->on = $on; + $this->field = $field; + $this->value = $value; + } +} diff --git a/src/Mapping/Annotation/Language.php b/src/Mapping/Annotation/Language.php new file mode 100644 index 0000000000..019f3d7543 --- /dev/null +++ b/src/Mapping/Annotation/Language.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Language annotation for Translatable behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class Language implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/Locale.php b/src/Mapping/Annotation/Locale.php new file mode 100644 index 0000000000..7a18317eca --- /dev/null +++ b/src/Mapping/Annotation/Locale.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Locale annotation for Translatable behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class Locale implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/Loggable.php b/src/Mapping/Annotation/Loggable.php new file mode 100644 index 0000000000..3af5db773c --- /dev/null +++ b/src/Mapping/Annotation/Loggable.php @@ -0,0 +1,66 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Loggable\LogEntryInterface; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Loggable annotation for Loggable behavioral extension + * + * @phpstan-template T of LogEntryInterface + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("CLASS") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +final class Loggable implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** + * @var string|null + * + * @phpstan-var class-string|null + */ + public $logEntryClass; + + /** + * @param array $data + * + * @phpstan-param class-string|null $logEntryClass + */ + public function __construct(array $data = [], ?string $logEntryClass = null) + { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2357', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->logEntryClass = $this->getAttributeValue($data, 'logEntryClass', $args, 1, $logEntryClass); + + return; + } + + $this->logEntryClass = $logEntryClass; + } +} diff --git a/src/Mapping/Annotation/Reference.php b/src/Mapping/Annotation/Reference.php new file mode 100644 index 0000000000..c1540222b4 --- /dev/null +++ b/src/Mapping/Annotation/Reference.php @@ -0,0 +1,95 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Reference annotation for ORM -> ODM references extension + * to be user like "@ReferenceMany(type="entity", class="MyEntity", identifier="entity_id")" + * + * @author Bulat Shakirzyanov + * + * @Annotation + */ +abstract class Reference implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** + * @var string|null + * + * @phpstan-var 'entity'|'document'|null + */ + public $type; + + /** + * @var string|null + * + * @phpstan-var class-string|null + */ + public $class; + + /** + * @var string|null + */ + public $identifier; + + /** + * @var string|null + */ + public $mappedBy; + + /** + * @var string|null + */ + public $inversedBy; + + /** + * @param array $data + * + * @phpstan-param class-string|null $class + */ + public function __construct( + array $data = [], + ?string $type = null, + ?string $class = null, + ?string $identifier = null, + ?string $mappedBy = null, + ?string $inversedBy = null + ) { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2389', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->type = $this->getAttributeValue($data, 'type', $args, 1, $type); + $this->class = $this->getAttributeValue($data, 'class', $args, 2, $class); + $this->identifier = $this->getAttributeValue($data, 'identifier', $args, 3, $identifier); + $this->mappedBy = $this->getAttributeValue($data, 'mappedBy', $args, 4, $mappedBy); + $this->inversedBy = $this->getAttributeValue($data, 'inversedBy', $args, 5, $inversedBy); + + return; + } + + $this->type = $type; + $this->class = $class; + $this->identifier = $identifier; + $this->mappedBy = $mappedBy; + $this->inversedBy = $inversedBy; + } +} diff --git a/src/Mapping/Annotation/ReferenceIntegrity.php b/src/Mapping/Annotation/ReferenceIntegrity.php new file mode 100644 index 0000000000..b986c0d2d8 --- /dev/null +++ b/src/Mapping/Annotation/ReferenceIntegrity.php @@ -0,0 +1,59 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * ReferenceIntegrity annotation for ReferenceIntegrity behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("PROPERTY") + * + * @author Evert Harmeling + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class ReferenceIntegrity implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** @var string|null */ + public $value; + + /** + * @param string|array|null $data + */ + public function __construct($data = [], ?string $value = null) + { + if (is_string($data)) { + $value = $data; + } elseif ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2389', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->value = $this->getAttributeValue($data, 'value', $args, 1, $value); + + return; + } + + $this->value = $value; + } +} diff --git a/src/Mapping/Annotation/ReferenceMany.php b/src/Mapping/Annotation/ReferenceMany.php new file mode 100644 index 0000000000..55dd82eecf --- /dev/null +++ b/src/Mapping/Annotation/ReferenceMany.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +/** + * Reference annotation for ORM -> ODM references extension + * to be user like "@ReferenceMany(type="entity", class="MyEntity", identifier="entity_id")" + * + * @author Bulat Shakirzyanov + * + * @NamedArgumentConstructor + * + * @Annotation + * + * @final since gedmo/doctrine-extensions 3.11 + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class ReferenceMany extends Reference +{ +} diff --git a/src/Mapping/Annotation/ReferenceManyEmbed.php b/src/Mapping/Annotation/ReferenceManyEmbed.php new file mode 100644 index 0000000000..211a7611be --- /dev/null +++ b/src/Mapping/Annotation/ReferenceManyEmbed.php @@ -0,0 +1,22 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +/** + * @NamedArgumentConstructor + * + * @Annotation + * + * @final since gedmo/doctrine-extensions 3.11 + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class ReferenceManyEmbed extends Reference +{ +} diff --git a/src/Mapping/Annotation/ReferenceOne.php b/src/Mapping/Annotation/ReferenceOne.php new file mode 100644 index 0000000000..1eb98bea1b --- /dev/null +++ b/src/Mapping/Annotation/ReferenceOne.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +/** + * Reference annotation for ORM -> ODM references extension + * to be user like "@ReferenceOne(type="entity", class="MyEntity", identifier="entity_id")" + * + * @author Bulat Shakirzyanov + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @final since gedmo/doctrine-extensions 3.11 + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class ReferenceOne extends Reference +{ +} diff --git a/src/Mapping/Annotation/Slug.php b/src/Mapping/Annotation/Slug.php new file mode 100644 index 0000000000..2d2c0acded --- /dev/null +++ b/src/Mapping/Annotation/Slug.php @@ -0,0 +1,113 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Slug annotation for Sluggable behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class Slug implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** + * @var string[] + * + * @Required + */ + public $fields = []; + public bool $updatable = true; + public string $style = 'default'; // or "camel" + public bool $unique = true; + public bool $uniqueOverTranslations = false; + /** @var string|null */ + public $unique_base; + public string $separator = '-'; + public string $prefix = ''; + public string $suffix = ''; + + /** + * @var SlugHandler[] + * + * @deprecated since gedmo/doctrine-extensions 3.18 + */ + public $handlers = []; + + public string $dateFormat = 'Y-m-d-H:i'; + + /** + * @param array $data + * @param string[] $fields + * @param SlugHandler[] $handlers @deprecated since since gedmo/doctrine-extensions 3.18 + */ + public function __construct( + array $data = [], + array $fields = [], + bool $updatable = true, + string $style = 'default', + bool $unique = true, + ?string $unique_base = null, + string $separator = '-', + string $prefix = '', + string $suffix = '', + array $handlers = [], + string $dateFormat = 'Y-m-d-H:i', + bool $uniqueOverTranslations = false + ) { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2379', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->fields = $this->getAttributeValue($data, 'fields', $args, 1, $fields); + $this->updatable = $this->getAttributeValue($data, 'updatable', $args, 2, $updatable); + $this->style = $this->getAttributeValue($data, 'style', $args, 3, $style); + $this->unique = $this->getAttributeValue($data, 'unique', $args, 4, $unique); + $this->unique_base = $this->getAttributeValue($data, 'unique_base', $args, 5, $unique_base); + $this->separator = $this->getAttributeValue($data, 'separator', $args, 6, $separator); + $this->prefix = $this->getAttributeValue($data, 'prefix', $args, 7, $prefix); + $this->suffix = $this->getAttributeValue($data, 'suffix', $args, 8, $suffix); + $this->handlers = $this->getAttributeValue($data, 'handlers', $args, 9, $handlers); + $this->dateFormat = $this->getAttributeValue($data, 'dateFormat', $args, 10, $dateFormat); + $this->uniqueOverTranslations = $this->getAttributeValue($data, 'uniqueOverTranslations', $args, 11, $uniqueOverTranslations); + + return; + } + + $this->fields = $fields; + $this->updatable = $updatable; + $this->style = $style; + $this->unique = $unique; + $this->uniqueOverTranslations = $uniqueOverTranslations; + $this->unique_base = $unique_base; + $this->separator = $separator; + $this->prefix = $prefix; + $this->suffix = $suffix; + $this->handlers = $handlers; + $this->dateFormat = $dateFormat; + } +} diff --git a/src/Mapping/Annotation/SlugHandler.php b/src/Mapping/Annotation/SlugHandler.php new file mode 100644 index 0000000000..a0819ebcfb --- /dev/null +++ b/src/Mapping/Annotation/SlugHandler.php @@ -0,0 +1,71 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; +use Gedmo\Sluggable\Handler\SlugHandlerInterface; + +/** + * SlugHandler annotation for Sluggable behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] +final class SlugHandler implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** + * @phpstan-var string|class-string + */ + public string $class = ''; + + /** + * @var array|array + */ + public array $options = []; + + /** + * @param array $data + * + * @phpstan-param string|class-string $class + * @phpstan-param array|array $options + */ + public function __construct( + array $data = [], + string $class = '', + array $options = [] + ) { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2379', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->class = $this->getAttributeValue($data, 'class', $args, 1, $class); + $this->options = $this->getAttributeValue($data, 'options', $args, 2, $options); + + return; + } + + $this->class = $class; + $this->options = $options; + } +} diff --git a/src/Mapping/Annotation/SlugHandlerOption.php b/src/Mapping/Annotation/SlugHandlerOption.php new file mode 100644 index 0000000000..98086e6d79 --- /dev/null +++ b/src/Mapping/Annotation/SlugHandlerOption.php @@ -0,0 +1,67 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * SlugHandlerOption annotation for Sluggable behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @author Gediminas Morkevicius + * + * @deprecated since gedmo/doctrine-extensions 3.18, will be removed in version 4.0. Configure the options as + * an associative array on the {@see SlugHandler} attribute instead. + */ +final class SlugHandlerOption implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + public string $name; + + /** + * @var mixed + */ + public $value; + + /** + * @param array $data + * @param mixed $value + */ + public function __construct( + array $data = [], + string $name = '', + $value = null + ) { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2379', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->name = $this->getAttributeValue($data, 'name', $args, 1, $name); + $this->value = $this->getAttributeValue($data, 'value', $args, 2, $value); + + return; + } + + $this->name = $name; + $this->value = $value; + } +} diff --git a/src/Mapping/Annotation/SoftDeleteable.php b/src/Mapping/Annotation/SoftDeleteable.php new file mode 100644 index 0000000000..709d9c4e7f --- /dev/null +++ b/src/Mapping/Annotation/SoftDeleteable.php @@ -0,0 +1,64 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Group annotation for SoftDeleteable extension + * + * @author Gustavo Falco + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("CLASS") + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +final class SoftDeleteable implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + public string $fieldName = 'deletedAt'; + + public bool $timeAware = false; + + public bool $hardDelete = true; + + /** + * @param array $data + */ + public function __construct(array $data = [], string $fieldName = 'deletedAt', bool $timeAware = false, bool $hardDelete = true) + { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2374', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->fieldName = $this->getAttributeValue($data, 'fieldName', $args, 1, $fieldName); + $this->timeAware = $this->getAttributeValue($data, 'timeAware', $args, 2, $timeAware); + $this->hardDelete = $this->getAttributeValue($data, 'hardDelete', $args, 3, $hardDelete); + + return; + } + + $this->fieldName = $fieldName; + $this->timeAware = $timeAware; + $this->hardDelete = $hardDelete; + } +} diff --git a/src/Mapping/Annotation/SortableGroup.php b/src/Mapping/Annotation/SortableGroup.php new file mode 100644 index 0000000000..bb96846f0b --- /dev/null +++ b/src/Mapping/Annotation/SortableGroup.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Group annotation for Sortable extension + * + * @author Lukas Botsch + * + * @Annotation + * + * @Target("PROPERTY") + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class SortableGroup implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/SortablePosition.php b/src/Mapping/Annotation/SortablePosition.php new file mode 100644 index 0000000000..44ca9ec41a --- /dev/null +++ b/src/Mapping/Annotation/SortablePosition.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Position annotation for Sortable extension + * + * @author Lukas Botsch + * + * @Annotation + * + * @Target("PROPERTY") + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class SortablePosition implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/Timestampable.php b/src/Mapping/Annotation/Timestampable.php new file mode 100644 index 0000000000..eceff32be7 --- /dev/null +++ b/src/Mapping/Annotation/Timestampable.php @@ -0,0 +1,66 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Timestampable annotation for Timestampable behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class Timestampable implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + public string $on = 'update'; + /** @var string|string[] */ + public $field; + /** @var mixed */ + public $value; + + /** + * @param array $data + * @param string|string[] $field + * @param mixed $value + */ + public function __construct(array $data = [], string $on = 'update', $field = null, $value = null) + { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2325', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->on = $this->getAttributeValue($data, 'on', $args, 1, $on); + $this->field = $this->getAttributeValue($data, 'field', $args, 2, $field); + $this->value = $this->getAttributeValue($data, 'value', $args, 3, $value); + + return; + } + + $this->on = $on; + $this->field = $field; + $this->value = $value; + } +} diff --git a/src/Mapping/Annotation/Translatable.php b/src/Mapping/Annotation/Translatable.php new file mode 100644 index 0000000000..0194efbd91 --- /dev/null +++ b/src/Mapping/Annotation/Translatable.php @@ -0,0 +1,57 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Translatable annotation for Translatable behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class Translatable implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** @var bool|null */ + public $fallback; + + /** + * @param array $data + */ + public function __construct(array $data = [], ?bool $fallback = null) + { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2253', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->fallback = $this->getAttributeValue($data, 'fallback', $args, 1, $fallback); + + return; + } + + $this->fallback = $fallback; + } +} diff --git a/src/Mapping/Annotation/TranslationEntity.php b/src/Mapping/Annotation/TranslationEntity.php new file mode 100644 index 0000000000..264b099cc2 --- /dev/null +++ b/src/Mapping/Annotation/TranslationEntity.php @@ -0,0 +1,57 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * TranslationEntity annotation for Translatable behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("CLASS") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +final class TranslationEntity implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** @Required */ + public string $class; + + /** + * @param array $data + */ + public function __construct(array $data = [], string $class = '') + { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2253', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->class = $this->getAttributeValue($data, 'class', $args, 1, $class); + + return; + } + + $this->class = $class; + } +} diff --git a/src/Mapping/Annotation/Tree.php b/src/Mapping/Annotation/Tree.php new file mode 100644 index 0000000000..f5af62d91f --- /dev/null +++ b/src/Mapping/Annotation/Tree.php @@ -0,0 +1,86 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Tree annotation for Tree behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("CLASS") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +final class Tree implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** + * @phpstan-var 'closure'|'materializedPath'|'nested' + */ + public string $type = 'nested'; + + public bool $activateLocking = false; + + /** + * @phpstan-var positive-int + */ + public int $lockingTimeout = 3; + + /** + * @var string|null + * + * @deprecated to be removed in 4.0, unused, configure the property on the TreeRoot annotation instead + */ + public $identifierMethod; + + /** + * @param array $data + * + * @phpstan-param 'closure'|'materializedPath'|'nested'|null $type + */ + public function __construct( + array $data = [], + ?string $type = null, + bool $activateLocking = false, + int $lockingTimeout = 3, + ?string $identifierMethod = null + ) { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2388', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->type = $this->getAttributeValue($data, 'type', $args, 1, $type); + $this->activateLocking = $this->getAttributeValue($data, 'activateLocking', $args, 2, $activateLocking); + $this->lockingTimeout = $this->getAttributeValue($data, 'lockingTimeout', $args, 3, $lockingTimeout); + $this->identifierMethod = $this->getAttributeValue($data, 'identifierMethod', $args, 4, $identifierMethod); + + return; + } + + $this->type = $type; + $this->activateLocking = $activateLocking; + $this->lockingTimeout = $lockingTimeout; + $this->identifierMethod = $identifierMethod; + } +} diff --git a/src/Mapping/Annotation/TreeClosure.php b/src/Mapping/Annotation/TreeClosure.php new file mode 100644 index 0000000000..f3397f4b23 --- /dev/null +++ b/src/Mapping/Annotation/TreeClosure.php @@ -0,0 +1,62 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; +use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; + +/** + * TreeClosure annotation for Tree behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("CLASS") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +final class TreeClosure implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** + * @phpstan-var string|class-string + */ + public string $class; + + /** + * @param array $data + * + * @phpstan-param string|class-string $class + */ + public function __construct(array $data = [], string $class = '') + { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2388', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->class = $this->getAttributeValue($data, 'class', $args, 1, $class); + + return; + } + + $this->class = $class; + } +} diff --git a/src/Mapping/Annotation/TreeLeft.php b/src/Mapping/Annotation/TreeLeft.php new file mode 100644 index 0000000000..ac3d0ede57 --- /dev/null +++ b/src/Mapping/Annotation/TreeLeft.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * TreeLeft annotation for Tree behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class TreeLeft implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/TreeLevel.php b/src/Mapping/Annotation/TreeLevel.php new file mode 100644 index 0000000000..021474ff78 --- /dev/null +++ b/src/Mapping/Annotation/TreeLevel.php @@ -0,0 +1,61 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * TreeLevel annotation for Tree behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class TreeLevel implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** + * The level which root nodes will have + */ + public int $base = 0; + + /** + * @param array $data + */ + public function __construct( + array $data = [], + int $base = 0 + ) { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2388', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->base = $this->getAttributeValue($data, 'base', $args, 1, $base); + + return; + } + + $this->base = $base; + } +} diff --git a/src/Mapping/Annotation/TreeLockTime.php b/src/Mapping/Annotation/TreeLockTime.php new file mode 100644 index 0000000000..13e56d93ba --- /dev/null +++ b/src/Mapping/Annotation/TreeLockTime.php @@ -0,0 +1,28 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * TreeLockTime annotation for Tree behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class TreeLockTime implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/TreeParent.php b/src/Mapping/Annotation/TreeParent.php new file mode 100644 index 0000000000..9a84bc8e57 --- /dev/null +++ b/src/Mapping/Annotation/TreeParent.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * TreeParent annotation for Tree behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class TreeParent implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/TreePath.php b/src/Mapping/Annotation/TreePath.php new file mode 100644 index 0000000000..d8f58d71ab --- /dev/null +++ b/src/Mapping/Annotation/TreePath.php @@ -0,0 +1,76 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * TreePath annotation for Tree behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("PROPERTY") + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * @author + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class TreePath implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + public string $separator = ','; + + /** @var bool|null */ + public $appendId; + + public bool $startsWithSeparator = false; + + public bool $endsWithSeparator = true; + + /** + * @param array $data + */ + public function __construct( + array $data = [], + string $separator = ',', + ?bool $appendId = null, + bool $startsWithSeparator = false, + bool $endsWithSeparator = true + ) { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2388', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->separator = $this->getAttributeValue($data, 'separator', $args, 1, $separator); + $this->appendId = $this->getAttributeValue($data, 'appendId', $args, 2, $appendId); + $this->startsWithSeparator = $this->getAttributeValue($data, 'startsWithSeparator', $args, 3, $startsWithSeparator); + $this->endsWithSeparator = $this->getAttributeValue($data, 'endsWithSeparator', $args, 4, $endsWithSeparator); + + return; + } + + $this->separator = $separator; + $this->appendId = $appendId; + $this->startsWithSeparator = $startsWithSeparator; + $this->endsWithSeparator = $endsWithSeparator; + } +} diff --git a/src/Mapping/Annotation/TreePathHash.php b/src/Mapping/Annotation/TreePathHash.php new file mode 100644 index 0000000000..9a1545d843 --- /dev/null +++ b/src/Mapping/Annotation/TreePathHash.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * TreePath annotation for Tree behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class TreePathHash implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/TreePathSource.php b/src/Mapping/Annotation/TreePathSource.php new file mode 100644 index 0000000000..5fa6ce5656 --- /dev/null +++ b/src/Mapping/Annotation/TreePathSource.php @@ -0,0 +1,28 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * TreePath annotation for Tree behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class TreePathSource implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/TreeRight.php b/src/Mapping/Annotation/TreeRight.php new file mode 100644 index 0000000000..727cf508f1 --- /dev/null +++ b/src/Mapping/Annotation/TreeRight.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * TreeRight annotation for Tree behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class TreeRight implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/TreeRoot.php b/src/Mapping/Annotation/TreeRoot.php new file mode 100644 index 0000000000..9920ef6e5c --- /dev/null +++ b/src/Mapping/Annotation/TreeRoot.php @@ -0,0 +1,57 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * TreeRoot annotation for Tree behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class TreeRoot implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + /** @var string|null */ + public $identifierMethod; + + /** + * @param array $data + */ + public function __construct(array $data = [], ?string $identifierMethod = null) + { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2388', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->identifierMethod = $this->getAttributeValue($data, 'identifierMethod', $args, 1, $identifierMethod); + + return; + } + + $this->identifierMethod = $identifierMethod; + } +} diff --git a/src/Mapping/Annotation/Uploadable.php b/src/Mapping/Annotation/Uploadable.php new file mode 100644 index 0000000000..dd58d9303c --- /dev/null +++ b/src/Mapping/Annotation/Uploadable.php @@ -0,0 +1,110 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Doctrine\Deprecations\Deprecation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; +use Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorInterface; +use Gedmo\Uploadable\Mapping\Validator; + +/** + * Uploadable annotation for Uploadable behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("CLASS") + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +final class Uploadable implements GedmoAnnotation +{ + use ForwardCompatibilityTrait; + + public bool $allowOverwrite = false; + + public bool $appendNumber = false; + + public string $path = ''; + + public string $pathMethod = ''; + + public string $callback = ''; + + /** + * @phpstan-var Validator::FILENAME_GENERATOR_*|class-string + */ + public string $filenameGenerator = Validator::FILENAME_GENERATOR_NONE; + + public string $maxSize = '0'; + + /** + * @var string A list of comma separate values of allowed types, like "text/plain,text/css" + */ + public string $allowedTypes = ''; + + /** + * @var string A list of comma separate values of disallowed types, like "video/jpeg,text/html" + */ + public string $disallowedTypes = ''; + + /** + * @param array $data + */ + public function __construct( + array $data = [], + bool $allowOverwrite = false, + bool $appendNumber = false, + string $path = '', + string $pathMethod = '', + string $callback = '', + string $filenameGenerator = Validator::FILENAME_GENERATOR_NONE, + string $maxSize = '0', + string $allowedTypes = '', + string $disallowedTypes = '' + ) { + if ([] !== $data) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2386', + 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', + __METHOD__ + ); + + $args = func_get_args(); + + $this->allowOverwrite = $this->getAttributeValue($data, 'allowOverwrite', $args, 1, $allowOverwrite); + $this->appendNumber = $this->getAttributeValue($data, 'appendNumber', $args, 2, $appendNumber); + $this->path = $this->getAttributeValue($data, 'path', $args, 3, $path); + $this->pathMethod = $this->getAttributeValue($data, 'pathMethod', $args, 4, $pathMethod); + $this->callback = $this->getAttributeValue($data, 'callback', $args, 5, $callback); + $this->filenameGenerator = $this->getAttributeValue($data, 'filenameGenerator', $args, 6, $filenameGenerator); + $this->maxSize = $this->getAttributeValue($data, 'maxSize', $args, 7, $maxSize); + $this->allowedTypes = $this->getAttributeValue($data, 'allowedTypes', $args, 8, $allowedTypes); + $this->disallowedTypes = $this->getAttributeValue($data, 'disallowedTypes', $args, 9, $disallowedTypes); + + return; + } + + $this->allowOverwrite = $allowOverwrite; + $this->appendNumber = $appendNumber; + $this->path = $path; + $this->pathMethod = $pathMethod; + $this->callback = $callback; + $this->filenameGenerator = $filenameGenerator; + $this->maxSize = $maxSize; + $this->allowedTypes = $allowedTypes; + $this->disallowedTypes = $disallowedTypes; + } +} diff --git a/src/Mapping/Annotation/UploadableFileMimeType.php b/src/Mapping/Annotation/UploadableFileMimeType.php new file mode 100644 index 0000000000..aac630ff66 --- /dev/null +++ b/src/Mapping/Annotation/UploadableFileMimeType.php @@ -0,0 +1,28 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * UploadableFileMimeType Annotation for Uploadable behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class UploadableFileMimeType implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/UploadableFileName.php b/src/Mapping/Annotation/UploadableFileName.php new file mode 100644 index 0000000000..0c5e10d94c --- /dev/null +++ b/src/Mapping/Annotation/UploadableFileName.php @@ -0,0 +1,27 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * UploadableFileName Annotation for Uploadable behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author tiger-seo + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class UploadableFileName implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/UploadableFilePath.php b/src/Mapping/Annotation/UploadableFilePath.php new file mode 100644 index 0000000000..8ec5f2e0af --- /dev/null +++ b/src/Mapping/Annotation/UploadableFilePath.php @@ -0,0 +1,28 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * UploadableFilePath Annotation for Uploadable behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class UploadableFilePath implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/UploadableFileSize.php b/src/Mapping/Annotation/UploadableFileSize.php new file mode 100644 index 0000000000..a0715bd78e --- /dev/null +++ b/src/Mapping/Annotation/UploadableFileSize.php @@ -0,0 +1,28 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * UploadableFileSize Annotation for Uploadable behavioral extension + * + * @Annotation + * + * @Target("PROPERTY") + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class UploadableFileSize implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Annotation/Versioned.php b/src/Mapping/Annotation/Versioned.php new file mode 100644 index 0000000000..db99b7bc07 --- /dev/null +++ b/src/Mapping/Annotation/Versioned.php @@ -0,0 +1,29 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Annotation; + +use Doctrine\Common\Annotations\Annotation; +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * Versioned annotation for Loggable behavioral extension + * + * @Annotation + * + * @NamedArgumentConstructor + * + * @Target("PROPERTY") + * + * @author Gediminas Morkevicius + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class Versioned implements GedmoAnnotation +{ +} diff --git a/src/Mapping/Driver.php b/src/Mapping/Driver.php new file mode 100644 index 0000000000..e4794cc690 --- /dev/null +++ b/src/Mapping/Driver.php @@ -0,0 +1,52 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping; + +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as OdmClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadata as OrmClassMetadata; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; +use Gedmo\Exception\InvalidMappingException; + +/** + * The mapping driver interface defines the metadata extraction functions + * common among all drivers used on these extensions. + * + * @author Gediminas Morkevicius + */ +interface Driver +{ + /** + * Read the extended metadata configuration for a single mapped class. + * + * @todo In the next major release stop receiving by reference the `$config` parameter and use `array` as return type declaration + * + * @param ClassMetadata $meta + * @param array $config + * + * @template T of object + * + * @phpstan-param ClassMetadata&(OdmClassMetadata|OrmClassMetadata) $meta + * + * @throws InvalidMappingException if the mapping configuration is invalid + * + * @return void + */ + public function readExtendedMetadata($meta, array &$config); + + /** + * Sets the original mapping driver. + * + * @param MappingDriver $driver + * + * @return void + */ + public function setOriginalDriver($driver); +} diff --git a/src/Mapping/Driver/AbstractAnnotationDriver.php b/src/Mapping/Driver/AbstractAnnotationDriver.php new file mode 100644 index 0000000000..eac276dff4 --- /dev/null +++ b/src/Mapping/Driver/AbstractAnnotationDriver.php @@ -0,0 +1,159 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Driver; + +use Doctrine\Common\Annotations\Reader; +use Doctrine\Deprecations\Deprecation; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; + +/** + * This is an abstract class to implement common functionality + * for extension annotation mapping drivers. + * + * @author Derek J. Lambert + */ +abstract class AbstractAnnotationDriver implements AttributeDriverInterface +{ + /** + * Annotation reader instance + * + * @var Reader|AttributeReader|object + * + * @todo Remove the support for the `object` type in the next major release. + */ + protected $reader; + + /** + * Original driver if it is available + * + * @var MappingDriver + */ + protected $_originalDriver; + + /** + * List of types which are valid for extension + * + * @var string[] + */ + protected $validTypes = []; + + /** + * Set the annotation reader instance + * + * When originally implemented, `Doctrine\Common\Annotations\Reader` was not available, + * therefore this method may accept any object implementing these methods from the interface: + * + * getClassAnnotations([reflectionClass]) + * getClassAnnotation([reflectionClass], [name]) + * getPropertyAnnotations([reflectionProperty]) + * getPropertyAnnotation([reflectionProperty], [name]) + * + * @param Reader|AttributeReader|object $reader + * + * @return void + * + * @note Providing any object is deprecated, as of 4.0 an {@see AttributeReader} will be required + */ + public function setAnnotationReader($reader) + { + if ($reader instanceof Reader) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2772', + 'Annotations support is deprecated, migrate your application to use attributes and pass an instance of %s to the %s() method instead.', + AttributeReader::class, + __METHOD__ + ); + } elseif (!$reader instanceof AttributeReader) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2558', + 'Providing an annotation reader which does not implement %s or is not an instance of %s to %s() is deprecated.', + Reader::class, + AttributeReader::class, + __METHOD__ + ); + } + + $this->reader = $reader; + } + + /** + * Passes in the mapping read by original driver + * + * @param MappingDriver $driver + * + * @return void + */ + public function setOriginalDriver($driver) + { + $this->_originalDriver = $driver; + } + + /** + * @param ClassMetadata $meta + * + * @return \ReflectionClass + */ + public function getMetaReflectionClass($meta) + { + return $meta->getReflectionClass(); + } + + /** + * @param ClassMetadata $meta + * @param array $config + * + * @return void + */ + public function validateFullMetadata(ClassMetadata $meta, array $config) + { + } + + /** + * Checks if $field type is valid + * + * @param ClassMetadata $meta + * @param string $field + * + * @return bool + */ + protected function isValidField($meta, $field) + { + $mapping = $meta->getFieldMapping($field); + + return $mapping && in_array($mapping->type ?? $mapping['type'], $this->validTypes, true); + } + + /** + * Try to find out related class name out of mapping + * + * @param ClassMetadata $metadata the mapped class metadata + * @param string $name the related object class name + * + * @phpstan-param class-string|string $name + * + * @return string related class name or empty string if does not exist + * + * @phpstan-return class-string|'' + */ + protected function getRelatedClassName($metadata, $name) + { + if (class_exists($name) || interface_exists($name)) { + return $name; + } + $refl = $metadata->getReflectionClass(); + $ns = $refl->getNamespaceName(); + $className = $ns.'\\'.$name; + + return class_exists($className) ? $className : ''; + } +} diff --git a/src/Mapping/Driver/AnnotationDriverInterface.php b/src/Mapping/Driver/AnnotationDriverInterface.php new file mode 100644 index 0000000000..f9ead4c668 --- /dev/null +++ b/src/Mapping/Driver/AnnotationDriverInterface.php @@ -0,0 +1,22 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Driver; + +/** + * Annotation driver interface, provides method + * to set custom annotation reader. + * + * @author Gediminas Morkevicius + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + */ +interface AnnotationDriverInterface extends AttributeDriverInterface +{ +} diff --git a/src/Mapping/Driver/AttributeAnnotationReader.php b/src/Mapping/Driver/AttributeAnnotationReader.php new file mode 100644 index 0000000000..ec0a70590d --- /dev/null +++ b/src/Mapping/Driver/AttributeAnnotationReader.php @@ -0,0 +1,109 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Driver; + +use Doctrine\Common\Annotations\Reader; +use Gedmo\Mapping\Annotation\Annotation; + +/** + * @author Gediminas Morkevicius + * + * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +final class AttributeAnnotationReader implements Reader +{ + private Reader $annotationReader; + + private AttributeReader $attributeReader; + + public function __construct(AttributeReader $attributeReader, Reader $annotationReader) + { + $this->attributeReader = $attributeReader; + $this->annotationReader = $annotationReader; + } + + /** + * @phpstan-param \ReflectionClass $class + * + * @return Annotation[] + */ + public function getClassAnnotations(\ReflectionClass $class): array + { + $annotations = $this->attributeReader->getClassAnnotations($class); + + if ([] !== $annotations) { + return $annotations; + } + + return $this->annotationReader->getClassAnnotations($class); + } + + /** + * @param string $annotationName + * + * @phpstan-param \ReflectionClass $class + * @phpstan-param class-string $annotationName the name of the annotation + * + * @return T|null the Annotation or NULL, if the requested annotation does not exist + * + * @template T + */ + public function getClassAnnotation(\ReflectionClass $class, $annotationName) + { + $annotation = $this->attributeReader->getClassAnnotation($class, $annotationName); + + return $annotation ?? $this->annotationReader->getClassAnnotation($class, $annotationName); + } + + /** + * @return Annotation[] + */ + public function getPropertyAnnotations(\ReflectionProperty $property): array + { + $propertyAnnotations = $this->attributeReader->getPropertyAnnotations($property); + + if ([] !== $propertyAnnotations) { + return $propertyAnnotations; + } + + return $this->annotationReader->getPropertyAnnotations($property); + } + + /** + * @param class-string $annotationName the name of the annotation + * + * @return T|null the Annotation or NULL, if the requested annotation does not exist + * + * @template T + */ + public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName) + { + $annotation = $this->attributeReader->getPropertyAnnotation($property, $annotationName); + + return $annotation ?? $this->annotationReader->getPropertyAnnotation($property, $annotationName); + } + + public function getMethodAnnotations(\ReflectionMethod $method): array + { + throw new \BadMethodCallException('Not implemented'); + } + + /** + * @return mixed + */ + public function getMethodAnnotation(\ReflectionMethod $method, $annotationName) + { + throw new \BadMethodCallException('Not implemented'); + } +} diff --git a/src/Mapping/Driver/AttributeDriverInterface.php b/src/Mapping/Driver/AttributeDriverInterface.php new file mode 100644 index 0000000000..12aa0fb1bd --- /dev/null +++ b/src/Mapping/Driver/AttributeDriverInterface.php @@ -0,0 +1,40 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Driver; + +use Doctrine\Common\Annotations\Reader; +use Gedmo\Mapping\Driver; + +/** + * @author Gediminas Morkevicius + * + * @internal + */ +interface AttributeDriverInterface extends Driver +{ + /** + * Set the annotation reader instance + * + * When originally implemented, `Doctrine\Common\Annotations\Reader` was not available, + * therefore this method may accept any object implementing these methods from the interface: + * + * getClassAnnotations([reflectionClass]) + * getClassAnnotation([reflectionClass], [name]) + * getPropertyAnnotations([reflectionProperty]) + * getPropertyAnnotation([reflectionProperty], [name]) + * + * @param Reader|AttributeReader|object $reader + * + * @return void + * + * @note Providing any object is deprecated, as of 4.0 an {@see AttributeReader} will be required + */ + public function setAnnotationReader($reader); +} diff --git a/src/Mapping/Driver/AttributeReader.php b/src/Mapping/Driver/AttributeReader.php new file mode 100644 index 0000000000..7e57396498 --- /dev/null +++ b/src/Mapping/Driver/AttributeReader.php @@ -0,0 +1,108 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Driver; + +use Gedmo\Mapping\Annotation\Annotation; + +/** + * @author Gediminas Morkevicius + */ +final class AttributeReader +{ + /** @var array */ + private array $isRepeatableAttribute = []; + + /** + * @phpstan-param \ReflectionClass $class + * + * @return array + */ + public function getClassAnnotations(\ReflectionClass $class): array + { + return $this->convertToAttributeInstances($class->getAttributes()); + } + + /** + * @phpstan-param \ReflectionClass $class + * @phpstan-param class-string $annotationName + * + * @return Annotation|Annotation[]|null + */ + public function getClassAnnotation(\ReflectionClass $class, string $annotationName) + { + return $this->getClassAnnotations($class)[$annotationName] ?? null; + } + + /** + * @return array + */ + public function getPropertyAnnotations(\ReflectionProperty $property): array + { + return $this->convertToAttributeInstances($property->getAttributes()); + } + + /** + * @phpstan-param class-string $annotationName + * + * @return Annotation|Annotation[]|null + */ + public function getPropertyAnnotation(\ReflectionProperty $property, string $annotationName) + { + return $this->getPropertyAnnotations($property)[$annotationName] ?? null; + } + + /** + * @param iterable<\ReflectionAttribute> $attributes + * + * @phpstan-param iterable<\ReflectionAttribute> $attributes + * + * @return array + */ + private function convertToAttributeInstances(iterable $attributes): array + { + $instances = []; + + foreach ($attributes as $attribute) { + $attributeName = $attribute->getName(); + assert(is_string($attributeName)); + // Make sure we only get Gedmo Annotations + if (!is_subclass_of($attributeName, Annotation::class)) { + continue; + } + + $instance = $attribute->newInstance(); + assert($instance instanceof Annotation); + + if ($this->isRepeatable($attributeName)) { + if (!isset($instances[$attributeName])) { + $instances[$attributeName] = []; + } + + $instances[$attributeName][] = $instance; + } else { + $instances[$attributeName] = $instance; + } + } + + return $instances; + } + + private function isRepeatable(string $attributeClassName): bool + { + if (isset($this->isRepeatableAttribute[$attributeClassName])) { + return $this->isRepeatableAttribute[$attributeClassName]; + } + + $reflectionClass = new \ReflectionClass($attributeClassName); + $attribute = $reflectionClass->getAttributes()[0]->newInstance(); + + return $this->isRepeatableAttribute[$attributeClassName] = ($attribute->flags & \Attribute::IS_REPEATABLE) > 0; + } +} diff --git a/src/Mapping/Driver/Chain.php b/src/Mapping/Driver/Chain.php new file mode 100644 index 0000000000..65d60bec26 --- /dev/null +++ b/src/Mapping/Driver/Chain.php @@ -0,0 +1,119 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Driver; + +use Gedmo\Mapping\Driver; + +/** + * The chain mapping driver enables chained + * extension mapping driver support + * + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class Chain implements Driver +{ + /** + * The default driver + */ + private ?Driver $defaultDriver = null; + + /** + * List of drivers nested + * + * @var array + */ + private array $_drivers = []; + + /** + * Add a nested driver. + * + * @param string $namespace + * + * @return void + */ + public function addDriver(Driver $nestedDriver, $namespace) + { + $this->_drivers[$namespace] = $nestedDriver; + } + + /** + * Get the array of nested drivers. + * + * @return array + */ + public function getDrivers() + { + return $this->_drivers; + } + + /** + * Get the default driver. + * + * @return Driver|null + */ + public function getDefaultDriver() + { + return $this->defaultDriver; + } + + /** + * Set the default driver. + * + * @return void + */ + public function setDefaultDriver(Driver $driver) + { + $this->defaultDriver = $driver; + } + + public function readExtendedMetadata($meta, array &$config) + { + foreach ($this->_drivers as $namespace => $driver) { + if (0 === strpos($meta->getName(), $namespace)) { + $extendedMetadata = $driver->readExtendedMetadata($meta, $config); + + if (\is_array($extendedMetadata)) { + $config = $extendedMetadata; + } + + // @todo: In the next major release remove the assignment to `$extendedMetadata`, the previous conditional + // block, uncomment the following line and replace the following return statement. + // return $driver->readExtendedMetadata($meta, $config); + return $config; + } + } + + if (null !== $this->defaultDriver) { + $extendedMetadata = $this->defaultDriver->readExtendedMetadata($meta, $config); + + if (\is_array($extendedMetadata)) { + $config = $extendedMetadata; + } + + // @todo: In the next major release remove the assignment to `$extendedMetadata`, the previous conditional + // block, uncomment the following line and replace the following return statement. + // return $this->defaultDriver->readExtendedMetadata($meta, $config); + return $config; + } + + // commenting it for customized mapping support, debugging of such cases might get harder + // throw new \Gedmo\Exception\UnexpectedValueException('Class ' . $meta->getName() . ' is not a valid entity or mapped super class.'); + } + + /** + * Passes in the mapping read by original driver + */ + public function setOriginalDriver($driver) + { + // not needed here + } +} diff --git a/lib/Gedmo/Mapping/Driver/File.php b/src/Mapping/Driver/File.php similarity index 55% rename from lib/Gedmo/Mapping/Driver/File.php rename to src/Mapping/Driver/File.php index 01985696ec..088ce6a25b 100644 --- a/lib/Gedmo/Mapping/Driver/File.php +++ b/src/Mapping/Driver/File.php @@ -1,12 +1,19 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Mapping\Driver; -use Doctrine\Common\Persistence\Mapping\Driver\FileDriver; -use Doctrine\Common\Persistence\Mapping\Driver\FileLocator; -use Doctrine\ORM\Mapping\Driver\AbstractFileDriver; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\Mapping\Driver\FileDriver; +use Doctrine\Persistence\Mapping\Driver\FileLocator; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Gedmo\Mapping\Driver; -use Gedmo\Exception\InvalidMappingException; /** * The mapping FileDriver abstract class, defines the @@ -15,7 +22,6 @@ * drivers. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ abstract class File implements Driver { @@ -26,15 +32,28 @@ abstract class File implements Driver /** * File extension, must be set in child class + * * @var string */ protected $_extension; /** * original driver if it is available + * + * @var MappingDriver + */ + protected $_originalDriver; + + /** + * @deprecated since gedmo/doctrine-extensions 3.3, will be removed in version 4.0. + * + * @var string[] */ - protected $_originalDriver = null; + protected $_paths = []; + /** + * @return void + */ public function setLocator(FileLocator $locator) { $this->locator = $locator; @@ -43,7 +62,9 @@ public function setLocator(FileLocator $locator) /** * Set the paths for file lookup * - * @param array $paths + * @deprecated since gedmo/doctrine-extensions 3.3, will be removed in version 4.0. + * + * @param string[] $paths * * @return void */ @@ -64,13 +85,27 @@ public function setExtension($extension) $this->_extension = $extension; } + /** + * Passes in the mapping read by original driver + * + * @param MappingDriver $driver + * + * @return void + */ + public function setOriginalDriver($driver) + { + $this->_originalDriver = $driver; + } + /** * Loads a mapping file with the given name and returns a map * from class/entity names to their corresponding elements. * - * @param string $file The mapping file to load. + * @param string $file the mapping file to load + * + * @return array|object|null> * - * @return array + * @phpstan-return array|object|null> */ abstract protected function _loadMappingFile($file); @@ -79,20 +114,22 @@ abstract protected function _loadMappingFile($file); * * @param string $className * - * @return null|array|object + * @phpstan-param class-string $className + * + * @return array|object|null */ protected function _getMapping($className) { - //try loading mapping from original driver first + // try loading mapping from original driver first $mapping = null; - if (!is_null($this->_originalDriver)) { - if ($this->_originalDriver instanceof FileDriver || $this->_originalDriver instanceof AbstractFileDriver) { + if (null !== $this->_originalDriver) { + if ($this->_originalDriver instanceof FileDriver) { $mapping = $this->_originalDriver->getElement($className); } } - //if no mapping found try to load mapping file again - if (is_null($mapping)) { + // if no mapping found try to load mapping file again + if (null === $mapping) { $yaml = $this->_loadMappingFile($this->locator->findMappingFile($className)); $mapping = $yaml[$className]; } @@ -101,23 +138,16 @@ protected function _getMapping($className) } /** - * Passes in the mapping read by original driver + * Try to find out related class name out of mapping * - * @param object $driver + * @param ClassMetadata $metadata the mapped class metadata + * @param string $name the related object class name * - * @return void - */ - public function setOriginalDriver($driver) - { - $this->_originalDriver = $driver; - } - - /** - * Try to find out related class name out of mapping + * @phpstan-param class-string|string $name * - * @param $metadata - the mapped class metadata - * @param $name - the related object class name - * @return string - related class name or empty string if does not exist + * @return string related class name or empty string if does not exist + * + * @phpstan-return class-string|'' */ protected function getRelatedClassName($metadata, $name) { @@ -126,7 +156,8 @@ protected function getRelatedClassName($metadata, $name) } $refl = $metadata->getReflectionClass(); $ns = $refl->getNamespaceName(); - $className = $ns . '\\' . $name; + $className = $ns.'\\'.$name; + return class_exists($className) ? $className : ''; } } diff --git a/lib/Gedmo/Mapping/Driver/Xml.php b/src/Mapping/Driver/Xml.php similarity index 62% rename from lib/Gedmo/Mapping/Driver/Xml.php rename to src/Mapping/Driver/Xml.php index a7fdb94d3f..e5bf767189 100644 --- a/lib/Gedmo/Mapping/Driver/Xml.php +++ b/src/Mapping/Driver/Xml.php @@ -1,10 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Mapping\Driver; -use Gedmo\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; -use SimpleXMLElement; /** * The mapping XmlDriver abstract class, defines the @@ -14,15 +19,15 @@ * * @author Miha Vrhovnik * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ abstract class Xml extends File { - const GEDMO_NAMESPACE_URI = 'http://gediminasm.org/schemas/orm/doctrine-extensions-mapping'; - const DOCTRINE_NAMESPACE_URI = 'http://doctrine-project.org/schemas/orm/doctrine-mapping'; + public const GEDMO_NAMESPACE_URI = 'http://gediminasm.org/schemas/orm/doctrine-extensions-mapping'; + public const DOCTRINE_NAMESPACE_URI = 'http://doctrine-project.org/schemas/orm/doctrine-mapping'; /** * File extension + * * @var string */ protected $_extension = '.dcm.xml'; @@ -31,12 +36,11 @@ abstract class Xml extends File * Get attribute value. * As we are supporting namespaces the only way to get to the attributes under a node is to use attributes function on it * - * @param SimpleXMLElement $node - * @param string $attributeName + * @param string $attributeName * * @return string */ - protected function _getAttribute(SimpleXmlElement $node, $attributeName) + protected function _getAttribute(\SimpleXMLElement $node, $attributeName) { $attributes = $node->attributes(); @@ -47,20 +51,20 @@ protected function _getAttribute(SimpleXmlElement $node, $attributeName) * Get boolean attribute value. * As we are supporting namespaces the only way to get to the attributes under a node is to use attributes function on it * - * @param SimpleXMLElement $node - * @param string $attributeName + * @param string $attributeName * - * @return boolean + * @return bool */ - protected function _getBooleanAttribute(SimpleXmlElement $node, $attributeName) + protected function _getBooleanAttribute(\SimpleXMLElement $node, $attributeName) { $rawValue = strtolower($this->_getAttribute($node, $attributeName)); - if ($rawValue === '1' || $rawValue === 'true') { + if ('1' === $rawValue || 'true' === $rawValue) { return true; } - if ($rawValue === '0' || $rawValue === 'false') { + if ('0' === $rawValue || 'false' === $rawValue) { return false; } + throw new InvalidMappingException(sprintf("Attribute %s must have a valid boolean value, '%s' found", $attributeName, $this->_getAttribute($node, $attributeName))); } @@ -68,25 +72,25 @@ protected function _getBooleanAttribute(SimpleXmlElement $node, $attributeName) * does attribute exist under a specific node * As we are supporting namespaces the only way to get to the attributes under a node is to use attributes function on it * - * @param SimpleXMLElement $node - * @param string $attributeName + * @param string $attributeName * - * @return string + * @return bool */ - protected function _isAttributeSet(SimpleXmlElement $node, $attributeName) + protected function _isAttributeSet(\SimpleXMLElement $node, $attributeName) { $attributes = $node->attributes(); return isset($attributes[$attributeName]); } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { - $result = array(); - $xmlElement = simplexml_load_file($file); + $result = []; + // We avoid calling `simplexml_load_file()` in order to prevent file operations in libXML. + // If `libxml_disable_entity_loader(true)` is called before, `simplexml_load_file()` fails, + // that's why we use `simplexml_load_string()` instead. + // @see https://bugs.php.net/bug.php?id=62577. + $xmlElement = simplexml_load_string(file_get_contents($file)); $xmlElement = $xmlElement->children(self::DOCTRINE_NAMESPACE_URI); if (isset($xmlElement->entity)) { diff --git a/src/Mapping/Event/Adapter/ODM.php b/src/Mapping/Event/Adapter/ODM.php new file mode 100644 index 0000000000..e434bfc611 --- /dev/null +++ b/src/Mapping/Event/Adapter/ODM.php @@ -0,0 +1,202 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Event\Adapter; + +use Doctrine\Common\EventArgs; +use Doctrine\Deprecations\Deprecation; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\UnitOfWork; +use Gedmo\Exception\RuntimeException; +use Gedmo\Mapping\Event\AdapterInterface; + +/** + * Doctrine event adapter for ODM specific + * event arguments + * + * @author Gediminas Morkevicius + */ +class ODM implements AdapterInterface +{ + private ?EventArgs $args = null; + + private ?DocumentManager $dm = null; + + private static ?bool $useIntId = null; + + public function __call($method, $args) + { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2409', + 'Using "%s()" method is deprecated since gedmo/doctrine-extensions 3.5 and will be removed in version 4.0.', + __METHOD__ + ); + + if (null === $this->args) { + throw new RuntimeException('Event args must be set before calling its methods'); + } + $method = str_replace('Object', $this->getDomainObjectName(), $method); + + return call_user_func_array([$this->args, $method], $args); + } + + public function setEventArgs(EventArgs $args) + { + $this->args = $args; + } + + public function getDomainObjectName() + { + return 'Document'; + } + + public function getManagerName() + { + return 'ODM'; + } + + /** + * @param ClassMetadata $meta + */ + public function getRootObjectClass($meta) + { + return $meta->rootDocumentName; + } + + /** + * Set the document manager + * + * @return void + */ + public function setDocumentManager(DocumentManager $dm) + { + $this->dm = $dm; + } + + /** + * @return DocumentManager + */ + public function getObjectManager() + { + if (null !== $this->dm) { + return $this->dm; + } + + if (null === $this->args) { + throw new \LogicException(sprintf('Event args must be set before calling "%s()".', __METHOD__)); + } + + return $this->args->getDocumentManager(); + } + + public function getObject(): object + { + if (null === $this->args) { + throw new \LogicException(sprintf('Event args must be set before calling "%s()".', __METHOD__)); + } + + return $this->args->getDocument(); + } + + public function getObjectState($uow, $object) + { + return $uow->getDocumentState($object); + } + + public function getObjectChangeSet($uow, $object) + { + return $uow->getDocumentChangeSet($object); + } + + /** + * @param ClassMetadata $meta + */ + public function getSingleIdentifierFieldName($meta) + { + return $meta->getIdentifier()[0]; + } + + /** + * @param ClassMetadata $meta + */ + public function recomputeSingleObjectChangeSet($uow, $meta, $object) + { + $uow->recomputeSingleDocumentChangeSet($meta, $object); + } + + public function getScheduledObjectUpdates($uow) + { + $updates = $uow->getScheduledDocumentUpdates(); + $upserts = $uow->getScheduledDocumentUpserts(); + + return array_merge($updates, $upserts); + } + + public function getScheduledObjectInsertions($uow) + { + return $uow->getScheduledDocumentInsertions(); + } + + public function getScheduledObjectDeletions($uow) + { + return $uow->getScheduledDocumentDeletions(); + } + + public function setOriginalObjectProperty($uow, $object, $property, $value) + { + $uow->setOriginalDocumentProperty($this->getOid($uow, $object), $property, $value); + } + + public function clearObjectChangeSet($uow, $object) + { + $uow->clearDocumentChangeSet($this->getOid($uow, $object)); + } + + /** + * @deprecated to be removed in 4.0, use custom lifecycle event classes instead. + * + * Creates a ODM specific LifecycleEventArgs. + * + * @param object $document + * @param DocumentManager $documentManager + * + * @return LifecycleEventArgs + */ + public function createLifecycleEventArgsInstance($document, $documentManager) + { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2649', + 'Using "%s()" method is deprecated since gedmo/doctrine-extensions 3.15 and will be removed in version 4.0.', + __METHOD__ + ); + + return new LifecycleEventArgs($document, $documentManager); + } + + /** + * @return int|string dependent on the version of `doctrine/mongodb-odm` installed + */ + private function getOid(UnitOfWork $uow, object $object) + { + if (null === self::$useIntId) { + $refl = new \ReflectionClass($uow); + $method = $refl->getMethod('setOriginalDocumentProperty'); + $oidArg = $method->getParameters()[0]; + + /** @phpstan-ignore-next-line method.NotFound All supported versions of `doctrine/mongodb-odm` have the first param typehinted */ + self::$useIntId = 'int' === $oidArg->getType()->getName(); + } + + return true === self::$useIntId ? spl_object_id($object) : spl_object_hash($object); + } +} diff --git a/src/Mapping/Event/Adapter/ORM.php b/src/Mapping/Event/Adapter/ORM.php new file mode 100644 index 0000000000..bca7cbade6 --- /dev/null +++ b/src/Mapping/Event/Adapter/ORM.php @@ -0,0 +1,218 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Event\Adapter; + +use Doctrine\Common\EventArgs; +use Doctrine\Deprecations\Deprecation; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\LifecycleEventArgs; +use Doctrine\ORM\Mapping\ClassMetadata; +use Gedmo\Exception\RuntimeException; +use Gedmo\Mapping\Event\AdapterInterface; + +/** + * Doctrine event adapter for ORM specific + * event arguments + * + * @author Gediminas Morkevicius + */ +class ORM implements AdapterInterface +{ + private ?EventArgs $args = null; + + private ?EntityManagerInterface $em = null; + + public function __call($method, $args) + { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2409', + 'Using "%s()" method is deprecated since gedmo/doctrine-extensions 3.5 and will be removed in version 4.0.', + __METHOD__ + ); + + if (null === $this->args) { + throw new RuntimeException('Event args must be set before calling its methods'); + } + $method = str_replace('Object', $this->getDomainObjectName(), $method); + + return call_user_func_array([$this->args, $method], $args); + } + + public function setEventArgs(EventArgs $args) + { + $this->args = $args; + } + + public function getDomainObjectName() + { + return 'Entity'; + } + + public function getManagerName() + { + return 'ORM'; + } + + /** + * @param ClassMetadata $meta + */ + public function getRootObjectClass($meta) + { + return $meta->rootEntityName; + } + + /** + * Set the entity manager + * + * @return void + */ + public function setEntityManager(EntityManagerInterface $em) + { + $this->em = $em; + } + + /** + * @return EntityManagerInterface + */ + public function getObjectManager() + { + if (null !== $this->em) { + return $this->em; + } + + if (null === $this->args) { + throw new \LogicException(sprintf('Event args must be set before calling "%s()".', __METHOD__)); + } + + // todo: for the next major release, uncomment the next line: + // return $this->args->getObjectManager(); + // and remove anything past this + if (\method_exists($this->args, 'getObjectManager')) { + return $this->args->getObjectManager(); + } + + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2639', + 'Calling "%s()" on event args of class "%s" that does not implement "getObjectManager()" is deprecated since gedmo/doctrine-extensions 3.14' + .' and will throw a "%s" error in version 4.0.', + __METHOD__, + get_class($this->args), + \Error::class + ); + + return $this->args->getEntityManager(); + } + + public function getObject(): object + { + if (null === $this->args) { + throw new \LogicException(sprintf('Event args must be set before calling "%s()".', __METHOD__)); + } + + // todo: for the next major release, uncomment the next line: + // return $this->args->getObject(); + // and remove anything past this + if (\method_exists($this->args, 'getObject')) { + return $this->args->getObject(); + } + + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2639', + 'Calling "%s()" on event args of class "%s" that does not imeplement "getObject()" is deprecated since gedmo/doctrine-extensions 3.14' + .' and will throw a "%s" error in version 4.0.', + __METHOD__, + get_class($this->args), + \Error::class + ); + + return $this->args->getEntity(); + } + + public function getObjectState($uow, $object) + { + return $uow->getEntityState($object); + } + + public function getObjectChangeSet($uow, $object) + { + return $uow->getEntityChangeSet($object); + } + + /** + * @param ClassMetadata $meta + */ + public function getSingleIdentifierFieldName($meta) + { + return $meta->getSingleIdentifierFieldName(); + } + + /** + * @param ClassMetadata $meta + */ + public function recomputeSingleObjectChangeSet($uow, $meta, $object) + { + $uow->recomputeSingleEntityChangeSet($meta, $object); + } + + public function getScheduledObjectUpdates($uow) + { + return $uow->getScheduledEntityUpdates(); + } + + public function getScheduledObjectInsertions($uow) + { + return $uow->getScheduledEntityInsertions(); + } + + public function getScheduledObjectDeletions($uow) + { + return $uow->getScheduledEntityDeletions(); + } + + public function setOriginalObjectProperty($uow, $object, $property, $value) + { + $uow->setOriginalEntityProperty(spl_object_id($object), $property, $value); + } + + public function clearObjectChangeSet($uow, $object) + { + $changeSet = &$uow->getEntityChangeSet($object); + $changeSet = []; + } + + /** + * @deprecated use custom lifecycle event classes instead + * + * Creates an ORM specific LifecycleEventArgs + * + * @param object $object + * @param EntityManagerInterface $entityManager + * + * @return LifecycleEventArgs + */ + public function createLifecycleEventArgsInstance($object, $entityManager) + { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2649', + 'Using "%s()" method is deprecated since gedmo/doctrine-extensions 3.15 and will be removed in version 4.0.', + __METHOD__ + ); + + if (!class_exists(LifecycleEventArgs::class)) { + throw new \RuntimeException(sprintf('Cannot call %s() when using doctrine/orm >=3.0, use a custom lifecycle event class instead.', __METHOD__)); + } + + return new LifecycleEventArgs($object, $entityManager); + } +} diff --git a/src/Mapping/Event/AdapterInterface.php b/src/Mapping/Event/AdapterInterface.php new file mode 100644 index 0000000000..7ce0f2f1a4 --- /dev/null +++ b/src/Mapping/Event/AdapterInterface.php @@ -0,0 +1,172 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Event; + +use Doctrine\Common\EventArgs; +use Doctrine\ODM\MongoDB\UnitOfWork as MongoDBUnitOfWork; +use Doctrine\ORM\UnitOfWork as ORMUnitOfWork; +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; + +/** + * Doctrine event adapter for Doctrine extensions. + * + * @author Gediminas Morkevicius + * + * @method LifecycleEventArgs createLifecycleEventArgsInstance(object $object, ObjectManager $manager) @deprecated + * @method object getObject() + */ +interface AdapterInterface +{ + /** + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * Calls a method on the event args object. + * + * @param string $method + * @param array $args + * + * @return mixed + */ + public function __call($method, $args); + + /** + * Set the event args object. + * + * @return void + */ + public function setEventArgs(EventArgs $args); + + /** + * Get the name of the domain object. + * + * @return string + */ + public function getDomainObjectName(); + + /** + * Get the name of the manager used by this adapter. + * + * @return string + */ + public function getManagerName(); + + /** + * Get the root object class, handles inheritance + * + * @param ClassMetadata $meta + * + * @return string + * + * @phpstan-return class-string + */ + public function getRootObjectClass($meta); + + /** + * Get the object manager. + * + * @return ObjectManager + */ + public function getObjectManager(); + + /** + * Gets the state of an object from the unit of work. + * + * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager + * @param object $object + * + * @return int The object state as reported by the unit of work + */ + public function getObjectState($uow, $object); + + /** + * Gets the changeset for an object from the unit of work. + * + * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager + * @param object $object + * + * @return array|object> + * + * @phpstan-return array + */ + public function getObjectChangeSet($uow, $object); + + /** + * Get the single identifier field name. + * + * @param ClassMetadata $meta + * + * @return string + */ + public function getSingleIdentifierFieldName($meta); + + /** + * Computes the changeset of an individual object, independently of the + * computeChangeSets() routine that is used at the beginning of a unit + * of work's commit. + * + * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager + * @param ClassMetadata $meta + * @param object $object + * + * @return void + */ + public function recomputeSingleObjectChangeSet($uow, $meta, $object); + + /** + * Gets the currently scheduled object updates from the unit of work. + * + * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager + * + * @return array + */ + public function getScheduledObjectUpdates($uow); + + /** + * Gets the currently scheduled object insertions in the unit of work. + * + * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager + * + * @return array + */ + public function getScheduledObjectInsertions($uow); + + /** + * Gets the currently scheduled object deletions in the unit of work. + * + * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager + * + * @return array + */ + public function getScheduledObjectDeletions($uow); + + /** + * Sets a property value of the original data array of an object. + * + * @param ORMUnitOfWork|MongoDBUnitOfWork $uow + * @param object $object + * @param string $property + * @param mixed $value + * + * @return void + */ + public function setOriginalObjectProperty($uow, $object, $property, $value); + + /** + * Clears the property changeset of the object with the given OID. + * + * @param ORMUnitOfWork|MongoDBUnitOfWork $uow + * @param object $object + * + * @return void + */ + public function clearObjectChangeSet($uow, $object); +} diff --git a/src/Mapping/Event/ClockAwareAdapterInterface.php b/src/Mapping/Event/ClockAwareAdapterInterface.php new file mode 100644 index 0000000000..bf10534947 --- /dev/null +++ b/src/Mapping/Event/ClockAwareAdapterInterface.php @@ -0,0 +1,20 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping\Event; + +use Psr\Clock\ClockInterface; + +/** + * Doctrine event adapter supporting a PSR-20 {@see ClockInterface}. + */ +interface ClockAwareAdapterInterface +{ + public function setClock(ClockInterface $clock): void; +} diff --git a/src/Mapping/ExtensionMetadataFactory.php b/src/Mapping/ExtensionMetadataFactory.php new file mode 100644 index 0000000000..2e4d045b4a --- /dev/null +++ b/src/Mapping/ExtensionMetadataFactory.php @@ -0,0 +1,291 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping; + +use Doctrine\Bundle\DoctrineBundle\Mapping\MappingDriver as DoctrineBundleMappingDriver; +use Doctrine\Common\Annotations\Reader; +use Doctrine\Deprecations\Deprecation; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as DocumentClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadata as EntityClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataInfo as LegacyEntityClassMetadata; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\Mapping\Driver\DefaultFileLocator; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Doctrine\Persistence\Mapping\Driver\SymfonyFileLocator; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\RuntimeException; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; +use Gedmo\Mapping\Driver\AttributeAnnotationReader; +use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Mapping\Driver\AttributeReader; +use Gedmo\Mapping\Driver\Chain; +use Gedmo\Mapping\Driver\File as FileDriver; +use Psr\Cache\CacheItemPoolInterface; + +/** + * The extension metadata factory is responsible for extension driver + * initialization and fully reading the extension metadata + * + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class ExtensionMetadataFactory +{ + /** + * Extension driver + * + * @var Driver + */ + protected $driver; + + /** + * Object manager, entity or document + * + * @var ObjectManager + */ + protected $objectManager; + + /** + * Extension namespace + * + * @var string + */ + protected $extensionNamespace; + + /** + * Metadata annotation reader + * + * @var Reader|AttributeReader|object|null + */ + protected $annotationReader; + + private ?CacheItemPoolInterface $cacheItemPool = null; + + /** + * @param Reader|AttributeReader|object|null $annotationReader + * + * @note Providing any object as the third argument is deprecated, as of 4.0 an {@see AttributeReader} will be required + */ + public function __construct(ObjectManager $objectManager, string $extensionNamespace, ?object $annotationReader = null, ?CacheItemPoolInterface $cacheItemPool = null) + { + if (null !== $annotationReader) { + if ($annotationReader instanceof Reader) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2772', + 'Annotations support is deprecated, migrate your application to use attributes and pass an instance of %s to the %s constructor instead.', + AttributeReader::class, + static::class + ); + } elseif (!$annotationReader instanceof AttributeReader) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2258', + 'Providing an annotation reader which does not implement %s or is not an instance of %s to %s is deprecated.', + Reader::class, + AttributeReader::class, + static::class + ); + } + } + + $this->objectManager = $objectManager; + $this->annotationReader = $annotationReader; + $this->extensionNamespace = $extensionNamespace; + $omDriver = $objectManager->getConfiguration()->getMetadataDriverImpl(); + $this->driver = $this->getDriver($omDriver); + $this->cacheItemPool = $cacheItemPool; + } + + /** + * Reads extension metadata + * + * @param ClassMetadata&(DocumentClassMetadata|EntityClassMetadata|LegacyEntityClassMetadata) $meta + * + * @return array the metadata configuration + */ + public function getExtensionMetadata($meta) + { + if ($meta->isMappedSuperclass) { + return []; // ignore mappedSuperclasses for now + } + $config = []; + $cmf = $this->objectManager->getMetadataFactory(); + $useObjectName = $meta->getName(); + // collect metadata from inherited classes + if (null !== $meta->reflClass) { + foreach (array_reverse(class_parents($meta->getName())) as $parentClass) { + // read only inherited mapped classes + if ($cmf->hasMetadataFor($parentClass) || !$cmf->isTransient($parentClass)) { + assert(class_exists($parentClass)); + + $class = $this->objectManager->getClassMetadata($parentClass); + + assert($class instanceof DocumentClassMetadata || $class instanceof EntityClassMetadata || $class instanceof LegacyEntityClassMetadata); + + $extendedMetadata = $this->driver->readExtendedMetadata($class, $config); + + if (\is_array($extendedMetadata)) { + $config = $extendedMetadata; + } + + // @todo: In the next major release remove the assignment to `$extendedMetadata`, the previous conditional + // block and uncomment the following line. + // $config = $this->driver->readExtendedMetadata($class, $config); + + $isBaseInheritanceLevel = !$class->isInheritanceTypeNone() + && [] === $class->parentClasses + && [] !== $config + ; + if ($isBaseInheritanceLevel) { + $useObjectName = $class->getName(); + } + } + } + + $extendedMetadata = $this->driver->readExtendedMetadata($meta, $config); + + if (\is_array($extendedMetadata)) { + $config = $extendedMetadata; + } + + // @todo: In the next major release remove the assignment to `$extendedMetadata`, the previous conditional + // block and uncomment the following line. + // $config = $this->driver->readExtendedMetadata($meta, $config); + } + if ([] !== $config) { + $config['useObjectClass'] = $useObjectName; + } + + $this->storeConfiguration($meta->getName(), $config); + + return $config; + } + + /** + * Get the cache id + * + * @param string $className + * @param string $extensionNamespace + * + * @return string + */ + public static function getCacheId($className, $extensionNamespace) + { + return str_replace('\\', '_', $className).'__'.strtoupper(str_replace('\\', '_', $extensionNamespace)).'_CLASSMETADATA'; + } + + /** + * Get the extended driver instance which will + * read the metadata required by extension + * + * @param MappingDriver $omDriver + * + * @throws RuntimeException if driver was not found in extension + * + * @return Driver + */ + protected function getDriver($omDriver) + { + if ($omDriver instanceof DoctrineBundleMappingDriver) { + $omDriver = $omDriver->getDriver(); + } + + $driver = null; + $className = get_class($omDriver); + $driverName = substr($className, strrpos($className, '\\') + 1); + if ($omDriver instanceof MappingDriverChain || 'DriverChain' === $driverName) { + $driver = new Chain(); + foreach ($omDriver->getDrivers() as $namespace => $nestedOmDriver) { + $driver->addDriver($this->getDriver($nestedOmDriver), $namespace); + } + if (null !== $omDriver->getDefaultDriver()) { + $driver->setDefaultDriver($this->getDriver($omDriver->getDefaultDriver())); + } + } else { + $driverName = substr($driverName, 0, strpos($driverName, 'Driver')); + $isSimplified = false; + if ('Simplified' === substr($driverName, 0, 10)) { + // support for simplified file drivers + $driverName = substr($driverName, 10); + $isSimplified = true; + } + // create driver instance + $driverClassName = $this->extensionNamespace.'\Mapping\Driver\\'.$driverName; + if (!class_exists($driverClassName)) { + $originalDriverClassName = $driverClassName; + + // try to fall back to either an annotation or attribute driver depending on the available dependencies + if (interface_exists(Reader::class)) { + $driverClassName = $this->extensionNamespace.'\Mapping\Driver\Annotation'; + } elseif (\PHP_VERSION_ID >= 80000) { + $driverClassName = $this->extensionNamespace.'\Mapping\Driver\Attribute'; + } + + if (!class_exists($driverClassName)) { + if ($originalDriverClassName !== $driverClassName) { + throw new RuntimeException("Failed to create mapping driver: ({$originalDriverClassName}), the extension driver nor a fallback annotation or attribute driver could be found."); + } + + throw new RuntimeException("Failed to fallback to annotation driver: ({$driverClassName}), extension driver was not found."); + } + } + $driver = new $driverClassName(); + $driver->setOriginalDriver($omDriver); + if ($driver instanceof FileDriver) { + if ($omDriver instanceof MappingDriver) { + $driver->setLocator($omDriver->getLocator()); + // BC for Doctrine 2.2 + } elseif ($isSimplified) { + $driver->setLocator(new SymfonyFileLocator($omDriver->getNamespacePrefixes(), $omDriver->getFileExtension())); + } else { + $driver->setLocator(new DefaultFileLocator($omDriver->getPaths(), $omDriver->getFileExtension())); + } + } + + if ($driver instanceof AttributeDriverInterface) { + if (null === $this->annotationReader) { + throw new RuntimeException("Cannot use metadata driver ({$driverClassName}), an annotation or attribute reader was not provided."); + } + + if ($driver instanceof AnnotationDriverInterface) { + $driver->setAnnotationReader($this->annotationReader); + } else { + if ($this->annotationReader instanceof AttributeReader) { + $driver->setAnnotationReader($this->annotationReader); + } else { + $driver->setAnnotationReader(new AttributeAnnotationReader(new AttributeReader(), $this->annotationReader)); + } + } + } + } + + return $driver; + } + + /** + * @param array $config + */ + private function storeConfiguration(string $className, array $config): void + { + if (null === $this->cacheItemPool) { + return; + } + + // Cache the result, even if it's empty, to prevent re-parsing non-existent annotations. + $cacheId = self::getCacheId($className, $this->extensionNamespace); + + $item = $this->cacheItemPool->getItem($cacheId); + + $this->cacheItemPool->save($item->set($config)); + } +} diff --git a/src/Mapping/MappedEventSubscriber.php b/src/Mapping/MappedEventSubscriber.php new file mode 100644 index 0000000000..640e5cb984 --- /dev/null +++ b/src/Mapping/MappedEventSubscriber.php @@ -0,0 +1,365 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Mapping; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\PsrCachedReader; +use Doctrine\Common\Annotations\Reader; +use Doctrine\Common\EventArgs; +use Doctrine\Common\EventSubscriber; +use Doctrine\Deprecations\Deprecation; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as DocumentClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata as EntityClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataInfo as LegacyEntityClassMetadata; +use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Mapping\Driver\AttributeReader; +use Gedmo\Mapping\Event\AdapterInterface; +use Gedmo\Mapping\Event\ClockAwareAdapterInterface; +use Psr\Cache\CacheItemPoolInterface; +use Psr\Clock\ClockInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +/** + * This is extension of event subscriber class and is + * used specifically for handling the extension metadata + * mapping for extensions. + * + * It dries up some reusable code which is common for + * all extensions who maps additional metadata through + * extended drivers + * + * @phpstan-template TConfig of array + * @phpstan-template TEventAdapter of AdapterInterface + * + * @author Gediminas Morkevicius + */ +abstract class MappedEventSubscriber implements EventSubscriber +{ + /** + * Static List of cached object configurations + * leaving it static for reasons to look into + * other listener configuration + * + * @var array>> + * + * @phpstan-var array>> + */ + protected static $configurations = []; + + /** + * Listener name, etc: sluggable + * + * @var string + */ + protected $name; + + /** + * ExtensionMetadataFactory used to read the extension + * metadata through the extension drivers + * + * @var array + */ + private array $extensionMetadataFactory = []; + + /** + * List of event adapters used for this listener + * + * @var array + */ + private array $adapters = []; + + /** + * Custom annotation reader + * + * @var Reader|AttributeReader|object|false|null + */ + private $annotationReader = false; + + /** + * @var Reader|AttributeReader|false|null + */ + private static $defaultAnnotationReader = false; + + /** + * @var CacheItemPoolInterface|null + */ + private $cacheItemPool; + + private ?ClockInterface $clock = null; + + public function __construct() + { + $parts = explode('\\', $this->getNamespace()); + $this->name = end($parts); + } + + /** + * Get the configuration for specific object class + * if cache driver is present it scans it also + * + * @param string $class + * + * @phpstan-param class-string $class + * + * @return array + * + * @phpstan-return TConfig + */ + public function getConfiguration(ObjectManager $objectManager, $class) + { + if (isset(self::$configurations[$this->name][$class])) { + return self::$configurations[$this->name][$class]; + } + + $config = []; + + $cacheItemPool = $this->getCacheItemPool($objectManager); + + $cacheId = ExtensionMetadataFactory::getCacheId($class, $this->getNamespace()); + $cacheItem = $cacheItemPool->getItem($cacheId); + + if ($cacheItem->isHit()) { + $config = $cacheItem->get(); + self::$configurations[$this->name][$class] = $config; + } else { + // re-generate metadata on cache miss + $this->loadMetadataForObjectClass($objectManager, $objectManager->getClassMetadata($class)); + if (isset(self::$configurations[$this->name][$class])) { + $config = self::$configurations[$this->name][$class]; + } + } + + $objectClass = $config['useObjectClass'] ?? $class; + if ($objectClass !== $class) { + $this->getConfiguration($objectManager, $objectClass); + } + + return $config; + } + + /** + * Get extended metadata mapping reader + * + * @return ExtensionMetadataFactory + */ + public function getExtensionMetadataFactory(ObjectManager $objectManager) + { + $oid = spl_object_id($objectManager); + if (!isset($this->extensionMetadataFactory[$oid])) { + if (false === $this->annotationReader) { + // create default annotation/attribute reader for extensions + $this->annotationReader = $this->getDefaultAnnotationReader(); + } + $this->extensionMetadataFactory[$oid] = new ExtensionMetadataFactory( + $objectManager, + $this->getNamespace(), + $this->annotationReader, + $this->getCacheItemPool($objectManager) + ); + } + + return $this->extensionMetadataFactory[$oid]; + } + + /** + * Set the annotation reader instance + * + * When originally implemented, `Doctrine\Common\Annotations\Reader` was not available, + * therefore this method may accept any object implementing these methods from the interface: + * + * getClassAnnotations([reflectionClass]) + * getClassAnnotation([reflectionClass], [name]) + * getPropertyAnnotations([reflectionProperty]) + * getPropertyAnnotation([reflectionProperty], [name]) + * + * @param Reader|AttributeReader|object $reader + * + * @return void + * + * @note Providing any object is deprecated, as of 4.0 an {@see AttributeReader} will be required + */ + public function setAnnotationReader($reader) + { + if ($reader instanceof Reader) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2772', + 'Annotations support is deprecated, migrate your application to use attributes and pass an instance of %s to the %s() method instead.', + AttributeReader::class, + __METHOD__ + ); + } elseif (!$reader instanceof AttributeReader) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2558', + 'Providing an annotation reader which does not implement %s or is not an instance of %s to %s() is deprecated.', + Reader::class, + AttributeReader::class, + __METHOD__ + ); + } + + $this->annotationReader = $reader; + } + + final public function setCacheItemPool(CacheItemPoolInterface $cacheItemPool): void + { + $this->cacheItemPool = $cacheItemPool; + } + + final public function setClock(ClockInterface $clock): void + { + $this->clock = $clock; + } + + /** + * Scans the objects for extended annotations + * event subscribers must subscribe to loadClassMetadata event + * + * @param ClassMetadata $metadata + * + * @return void + */ + public function loadMetadataForObjectClass(ObjectManager $objectManager, $metadata) + { + assert($metadata instanceof DocumentClassMetadata || $metadata instanceof EntityClassMetadata || $metadata instanceof LegacyEntityClassMetadata); + + $factory = $this->getExtensionMetadataFactory($objectManager); + + try { + $config = $factory->getExtensionMetadata($metadata); + } catch (\ReflectionException $e) { + // entity\document generator is running + $config = []; // will not store a cached version, to remap later + } + if ([] !== $config) { + self::$configurations[$this->name][$metadata->getName()] = $config; + } + } + + /** + * Get an event adapter to handle event specific + * methods + * + * @throws InvalidArgumentException if event is not recognized + * + * @return AdapterInterface + * + * @phpstan-return TEventAdapter + */ + protected function getEventAdapter(EventArgs $args) + { + $class = get_class($args); + if (preg_match('@Doctrine\\\([^\\\]+)@', $class, $m) && in_array($m[1], ['ODM', 'ORM'], true)) { + if (!isset($this->adapters[$m[1]])) { + $adapterClass = $this->getNamespace().'\\Mapping\\Event\\Adapter\\'.$m[1]; + if (!\class_exists($adapterClass)) { + $adapterClass = 'Gedmo\\Mapping\\Event\\Adapter\\'.$m[1]; + } + $this->adapters[$m[1]] = new $adapterClass(); + + if ($this->adapters[$m[1]] instanceof ClockAwareAdapterInterface && $this->clock instanceof ClockInterface) { + $this->adapters[$m[1]]->setClock($this->clock); + } + } + $this->adapters[$m[1]]->setEventArgs($args); + + return $this->adapters[$m[1]]; + } + + throw new InvalidArgumentException('Event mapper does not support event arg class: '.$class); + } + + /** + * Get the namespace of extension event subscriber. + * used for cache id of extensions also to know where + * to find Mapping drivers and event adapters + * + * @return string + */ + abstract protected function getNamespace(); + + /** + * Sets the value for a mapped field + * + * @param object $object + * @param string $field + * @param mixed $oldValue + * @param mixed $newValue + * + * @return void + */ + protected function setFieldValue(AdapterInterface $adapter, $object, $field, $oldValue, $newValue) + { + $manager = $adapter->getObjectManager(); + $meta = $manager->getClassMetadata(get_class($object)); + $uow = $manager->getUnitOfWork(); + + $meta->setFieldValue($object, $field, $newValue); + $uow->propertyChanged($object, $field, $oldValue, $newValue); + $adapter->recomputeSingleObjectChangeSet($uow, $meta, $object); + } + + /** + * Get the default annotation or attribute reader for extensions, creating it if necessary. + * + * If a reader cannot be created due to missing requirements, no default will be set as the reader is only required for annotation or attribute metadata, + * and the {@see ExtensionMetadataFactory} can handle raising an error if it tries to create a mapping driver that requires this reader. + * + * @return Reader|AttributeReader|null + */ + private function getDefaultAnnotationReader() + { + if (false === self::$defaultAnnotationReader) { + if (class_exists(PsrCachedReader::class)) { + self::$defaultAnnotationReader = new PsrCachedReader(new AnnotationReader(), new ArrayAdapter()); + } elseif (\PHP_VERSION_ID >= 80000) { + self::$defaultAnnotationReader = new AttributeReader(); + } else { + self::$defaultAnnotationReader = null; + } + } + + return self::$defaultAnnotationReader; + } + + private function getCacheItemPool(ObjectManager $objectManager): CacheItemPoolInterface + { + if (null !== $this->cacheItemPool) { + return $this->cacheItemPool; + } + + // TODO: The user should configure its own cache, we are using the one from Doctrine for BC. We should deprecate using + // the one from Doctrine when the bundle offers an easy way to configure this cache, otherwise users using the bundle + // will see lots of deprecations without an easy way to avoid them. + + if ($objectManager instanceof EntityManagerInterface || $objectManager instanceof DocumentManager) { + $metadataFactory = $objectManager->getMetadataFactory(); + $getCache = \Closure::bind(static fn (AbstractClassMetadataFactory $metadataFactory): ?CacheItemPoolInterface => $metadataFactory->getCache(), null, \get_class($metadataFactory)); + + $metadataCache = $getCache($metadataFactory); + + if (null !== $metadataCache) { + $this->cacheItemPool = $metadataCache; + + return $this->cacheItemPool; + } + } + + $this->cacheItemPool = new ArrayAdapter(); + + return $this->cacheItemPool; + } +} diff --git a/src/ReferenceIntegrity/Mapping/Driver/Annotation.php b/src/ReferenceIntegrity/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..7637215bb9 --- /dev/null +++ b/src/ReferenceIntegrity/Mapping/Driver/Annotation.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\ReferenceIntegrity\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the reference integrity extension which reads extended metadata from annotations on a class with referential integrity. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/src/ReferenceIntegrity/Mapping/Driver/Attribute.php b/src/ReferenceIntegrity/Mapping/Driver/Attribute.php new file mode 100644 index 0000000000..e2b7387e01 --- /dev/null +++ b/src/ReferenceIntegrity/Mapping/Driver/Attribute.php @@ -0,0 +1,69 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\ReferenceIntegrity\Mapping\Driver; + +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\ReferenceIntegrity; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\ReferenceIntegrity\Mapping\Validator; + +/** + * Mapping driver for the reference integrity extension which reads extended metadata from attributes on a class with referential integrity. + * + * @author Evert Harmeling + * + * @internal + */ +class Attribute extends AbstractAnnotationDriver +{ + /** + * Mapping object for the reference integrity extension. + */ + public const REFERENCE_INTEGRITY = ReferenceIntegrity::class; + + /** + * Unimplemented mapping object for actions within the reference integrity extension. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0; actions are defined as {@see Validator} class constants instead. + */ + public const ACTION = 'Gedmo\\Mapping\\Annotation\\ReferenceIntegrityAction'; + + public function readExtendedMetadata($meta, array &$config) + { + $validator = new Validator(); + $reflClass = $this->getMetaReflectionClass($meta); + + foreach ($reflClass->getProperties() as $reflProperty) { + if ($referenceIntegrity = $this->reader->getPropertyAnnotation($reflProperty, self::REFERENCE_INTEGRITY)) { + \assert($referenceIntegrity instanceof ReferenceIntegrity); + + $property = $reflProperty->getName(); + + if (!$meta->hasField($property)) { + throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $property, $meta->getName())); + } + + $fieldMapping = $meta->getFieldMapping($property); + + if (!isset($fieldMapping['mappedBy'])) { + throw new InvalidMappingException(sprintf("'mappedBy' should be set on '%s' in '%s'", $property, $meta->getName())); + } + + if (!in_array($referenceIntegrity->value, $validator->getIntegrityActions(), true)) { + throw new InvalidMappingException(sprintf('Field - [%s] does not have a valid integrity option, [%s] in class - %s', $property, implode(', ', $validator->getIntegrityActions()), $meta->getName())); + } + + $config['referenceIntegrity'][$property] = $referenceIntegrity->value; + } + } + + return $config; + } +} diff --git a/lib/Gedmo/ReferenceIntegrity/Mapping/Driver/Yaml.php b/src/ReferenceIntegrity/Mapping/Driver/Yaml.php similarity index 52% rename from lib/Gedmo/ReferenceIntegrity/Mapping/Driver/Yaml.php rename to src/ReferenceIntegrity/Mapping/Driver/Yaml.php index dd156e2834..4c0bd33fc8 100644 --- a/lib/Gedmo/ReferenceIntegrity/Mapping/Driver/Yaml.php +++ b/src/ReferenceIntegrity/Mapping/Driver/Yaml.php @@ -1,10 +1,17 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\ReferenceIntegrity\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; -use Gedmo\Mapping\Driver\File; use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; use Gedmo\ReferenceIntegrity\Mapping\Validator; /** @@ -14,56 +21,38 @@ * extension. * * @author Evert Harmeling - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ class Yaml extends File implements Driver { /** * File extension + * * @var string */ protected $_extension = '.dcm.yml'; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); $validator = new Validator(); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $property => $fieldMapping) { if (isset($fieldMapping['gedmo']['referenceIntegrity'])) { if (!$meta->hasField($property)) { - throw new InvalidMappingException( - sprintf( - "Unable to find reference integrity [%s] as mapped property in entity - %s", - $property, - $meta->name - ) - ); + throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $property, $meta->getName())); } if (empty($mapping['fields'][$property]['mappedBy'])) { - throw new InvalidMappingException( - sprintf( - "'mappedBy' should be set on '%s' in '%s'", - $property, - $meta->name - ) - ); + throw new InvalidMappingException(sprintf("'mappedBy' should be set on '%s' in '%s'", $property, $meta->getName())); } - if (!in_array($fieldMapping['gedmo']['referenceIntegrity'], $validator->getIntegrityActions())) { - throw new InvalidMappingException( - sprintf( - "Field - [%s] does not have a valid integrity option, [%s] in class - %s", - $property, - implode($validator->getIntegrityActions(), ', '), - $meta->name - ) - ); + if (!in_array($fieldMapping['gedmo']['referenceIntegrity'], $validator->getIntegrityActions(), true)) { + throw new InvalidMappingException(sprintf('Field - [%s] does not have a valid integrity option, [%s] in class - %s', $property, implode(', ', $validator->getIntegrityActions()), $meta->getName())); } $config['referenceIntegrity'][$property][$mapping['fields'][$property]['mappedBy']] = @@ -71,11 +60,10 @@ public function readExtendedMetadata($meta, array &$config) } } } + + return $config; } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); diff --git a/src/ReferenceIntegrity/Mapping/Validator.php b/src/ReferenceIntegrity/Mapping/Validator.php new file mode 100644 index 0000000000..20281e79a0 --- /dev/null +++ b/src/ReferenceIntegrity/Mapping/Validator.php @@ -0,0 +1,49 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\ReferenceIntegrity\Mapping; + +/** + * This class is used to validate mapping information + * + * @author Evert Harmeling + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class Validator +{ + public const NULLIFY = 'nullify'; + public const PULL = 'pull'; + public const RESTRICT = 'restrict'; + + /** + * List of actions which are valid as integrity check + * + * @var string[] + * + * @phpstan-var array + */ + public const INTEGRITY_ACTIONS = [ + self::NULLIFY, + self::PULL, + self::RESTRICT, + ]; + + /** + * Returns a list of available integrity actions + * + * @return string[] + * + * @phpstan-return array + */ + public function getIntegrityActions() + { + return self::INTEGRITY_ACTIONS; + } +} diff --git a/lib/Gedmo/ReferenceIntegrity/ReferenceIntegrity.php b/src/ReferenceIntegrity/ReferenceIntegrity.php similarity index 70% rename from lib/Gedmo/ReferenceIntegrity/ReferenceIntegrity.php rename to src/ReferenceIntegrity/ReferenceIntegrity.php index 2a74f84986..fbc3d606eb 100644 --- a/lib/Gedmo/ReferenceIntegrity/ReferenceIntegrity.php +++ b/src/ReferenceIntegrity/ReferenceIntegrity.php @@ -1,44 +1,48 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\ReferenceIntegrity; /** - * This interface is not necessary but can be implemented for - * Entities which in some cases needs to be identified te have - * ReferenceIntegrity checks + * Marker interface for objects which can be identified as requiring reference integrity checks. * * @author Evert Harmeling - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ interface ReferenceIntegrity { - /** + /* * ReferenceIntegrity expects certain settings to be required * in combination with an association */ - /** + /* * example * @ODM\ReferenceOne(targetDocument="Article", nullable="true", mappedBy="type") * @Gedmo\ReferenceIntegrity("nullify") * @var Article */ - /** + /* * example * @ODM\ReferenceOne(targetDocument="Article", nullable="true", mappedBy="type") * @Gedmo\ReferenceIntegrity("restrict") * @var Article */ - /** + /* * example * @ODM\ReferenceMany(targetDocument="Article", nullable="true", mappedBy="type") * @Gedmo\ReferenceIntegrity("nullify") * @var Doctrine\Common\Collections\ArrayCollection */ - /** + /* * example * @ODM\ReferenceMany(targetDocument="Article", nullable="true", mappedBy="type") * @Gedmo\ReferenceIntegrity("restrict") diff --git a/src/ReferenceIntegrity/ReferenceIntegrityListener.php b/src/ReferenceIntegrity/ReferenceIntegrityListener.php new file mode 100644 index 0000000000..f3502b04ed --- /dev/null +++ b/src/ReferenceIntegrity/ReferenceIntegrityListener.php @@ -0,0 +1,161 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\ReferenceIntegrity; + +use Doctrine\Common\EventArgs; +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Exception\ReferenceIntegrityStrictException; +use Gedmo\Mapping\Event\AdapterInterface; +use Gedmo\Mapping\MappedEventSubscriber; +use Gedmo\ReferenceIntegrity\Mapping\Validator; + +/** + * The ReferenceIntegrity listener handles the reference integrity on related documents + * + * @phpstan-extends MappedEventSubscriber + * + * @author Evert Harmeling + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class ReferenceIntegrityListener extends MappedEventSubscriber +{ + /** + * @return string[] + */ + public function getSubscribedEvents() + { + return [ + 'loadClassMetadata', + 'preRemove', + ]; + } + + /** + * Maps additional metadata for the Document + * + * @param LoadClassMetadataEventArgs $eventArgs + * + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $eventArgs + * + * @return void + */ + public function loadClassMetadata(EventArgs $eventArgs) + { + $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); + } + + /** + * Looks for referenced objects being removed + * to nullify the relation or throw an exception + * + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void + */ + public function preRemove(EventArgs $args) + { + $ea = $this->getEventAdapter($args); + $om = $ea->getObjectManager(); + $object = $ea->getObject(); + $class = get_class($object); + $meta = $om->getClassMetadata($class); + + if ($config = $this->getConfiguration($om, $meta->getName())) { + foreach ($config['referenceIntegrity'] as $property => $action) { + $refDoc = $meta->getFieldValue($object, $property); + $fieldMapping = $meta->getFieldMapping($property); + + switch ($action) { + case Validator::NULLIFY: + if (!isset($fieldMapping['mappedBy'])) { + throw new InvalidMappingException(sprintf("Reference '%s' on '%s' should have 'mappedBy' option defined", $property, $meta->getName())); + } + + assert(class_exists($fieldMapping->targetDocument ?? $fieldMapping['targetDocument'])); + + $subMeta = $om->getClassMetadata($fieldMapping->targetDocument ?? $fieldMapping['targetDocument']); + + $mappedByField = $fieldMapping->mappedBy ?? $fieldMapping['mappedBy']; + + if (!$subMeta->hasField($mappedByField)) { + throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $mappedByField, $fieldMapping->targetDocument ?? $fieldMapping['targetDocument'])); + } + + if ($meta->isCollectionValuedReference($property)) { + foreach ($refDoc as $refObj) { + $subMeta->setFieldValue($refObj, $mappedByField, null); + $om->persist($refObj); + } + } else { + $subMeta->setFieldValue($refDoc, $mappedByField, null); + $om->persist($refDoc); + } + + break; + case Validator::PULL: + if (!isset($fieldMapping['mappedBy'])) { + throw new InvalidMappingException(sprintf("Reference '%s' on '%s' should have 'mappedBy' option defined", $property, $meta->getName())); + } + + assert(class_exists($fieldMapping->targetDocument ?? $fieldMapping['targetDocument'])); + + $subMeta = $om->getClassMetadata($fieldMapping->targetDocument ?? $fieldMapping['targetDocument']); + + $mappedByField = $fieldMapping->mappedBy ?? $fieldMapping['mappedBy']; + + if (!$subMeta->hasField($mappedByField)) { + throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $mappedByField, $fieldMapping->targetDocument ?? $fieldMapping['targetDocument'])); + } + + if (!$subMeta->isCollectionValuedReference($mappedByField)) { + throw new InvalidMappingException(sprintf('Reference integrity [%s] mapped property in entity - %s should be a Reference Many', $mappedByField, $fieldMapping->targetDocument ?? $fieldMapping['targetDocument'])); + } + + if ($meta->isCollectionValuedReference($property)) { + foreach ($refDoc as $refObj) { + $collection = $subMeta->getFieldValue($refObj, $mappedByField); + $collection->removeElement($object); + $subMeta->setFieldValue($refObj, $mappedByField, $collection); + $om->persist($refObj); + } + } elseif (is_object($refDoc)) { + $collection = $subMeta->getFieldValue($refDoc, $mappedByField); + $collection->removeElement($object); + $subMeta->setFieldValue($refDoc, $mappedByField, $collection); + $om->persist($refDoc); + } + + break; + case Validator::RESTRICT: + if ($meta->isCollectionValuedReference($property) && $refDoc->count() > 0) { + throw new ReferenceIntegrityStrictException(sprintf("The reference integrity for the '%s' collection is restricted", $fieldMapping->targetDocument ?? $fieldMapping['targetDocument'])); + } + if ($meta->isSingleValuedReference($property) && null !== $refDoc) { + throw new ReferenceIntegrityStrictException(sprintf("The reference integrity for the '%s' document is restricted", $fieldMapping->targetDocument ?? $fieldMapping['targetDocument'])); + } + + break; + } + } + } + } + + protected function getNamespace() + { + return __NAMESPACE__; + } +} diff --git a/src/References/LazyCollection.php b/src/References/LazyCollection.php new file mode 100644 index 0000000000..f55484f37e --- /dev/null +++ b/src/References/LazyCollection.php @@ -0,0 +1,44 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\References; + +use Doctrine\Common\Collections\AbstractLazyCollection; + +/** + * Lazy collection for loading reference many associations. + * + * @author Gediminas Morkevicius + * @author Bulat Shakirzyanov + * @author Jonathan H. Wage + * + * @template-extends AbstractLazyCollection + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class LazyCollection extends AbstractLazyCollection +{ + /** + * @var callable + */ + private $callback; + + /** + * @param callable $callback + */ + public function __construct($callback) + { + $this->callback = $callback; + } + + protected function doInitialize(): void + { + $this->collection = call_user_func($this->callback); + } +} diff --git a/src/References/Mapping/Driver/Annotation.php b/src/References/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..3382d254e8 --- /dev/null +++ b/src/References/Mapping/Driver/Annotation.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\References\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the references extension which reads extended metadata from annotations on a class with references. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/src/References/Mapping/Driver/Attribute.php b/src/References/Mapping/Driver/Attribute.php new file mode 100644 index 0000000000..0e1450c709 --- /dev/null +++ b/src/References/Mapping/Driver/Attribute.php @@ -0,0 +1,85 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\References\Mapping\Driver; + +use Gedmo\Mapping\Annotation\Reference; +use Gedmo\Mapping\Annotation\ReferenceMany; +use Gedmo\Mapping\Annotation\ReferenceManyEmbed; +use Gedmo\Mapping\Annotation\ReferenceOne; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; + +/** + * Mapping driver for the references extension which reads extended metadata from attributes on a class with references. + * + * @author Gediminas Morkevicius + * @author Bulat Shakirzyanov + * @author Jonathan H. Wage + * + * @internal + */ +class Attribute extends AbstractAnnotationDriver +{ + /** + * Mapping object declaring a field as having a reference to one object. + */ + public const REFERENCE_ONE = ReferenceOne::class; + + /** + * Mapping object declaring a field as having a reference to many objects. + */ + public const REFERENCE_MANY = ReferenceMany::class; + + /** + * Mapping object declaring a field as having a reference to an embedded collection of many objects. + */ + public const REFERENCE_MANY_EMBED = ReferenceManyEmbed::class; + + /** + * @var array + */ + private const ANNOTATIONS = [ + 'referenceOne' => self::REFERENCE_ONE, + 'referenceMany' => self::REFERENCE_MANY, + 'referenceManyEmbed' => self::REFERENCE_MANY_EMBED, + ]; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $meta->getReflectionClass(); + + foreach (self::ANNOTATIONS as $key => $annotation) { + $config[$key] = []; + + foreach ($class->getProperties() as $property) { + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + if ($reference = $this->reader->getPropertyAnnotation($property, $annotation)) { + \assert($reference instanceof Reference); + + $config[$key][$property->getName()] = [ + 'field' => $property->getName(), + 'type' => $reference->type, + 'class' => $reference->class, + 'identifier' => $reference->identifier, + 'mappedBy' => $reference->mappedBy, + 'inversedBy' => $reference->inversedBy, + ]; + } + } + } + + return $config; + } +} diff --git a/lib/Gedmo/References/Mapping/Driver/Xml.php b/src/References/Mapping/Driver/Xml.php similarity index 58% rename from lib/Gedmo/References/Mapping/Driver/Xml.php rename to src/References/Mapping/Driver/Xml.php index 35b30ee2ae..79d5b1ab7d 100644 --- a/lib/Gedmo/References/Mapping/Driver/Xml.php +++ b/src/References/Mapping/Driver/Xml.php @@ -1,9 +1,16 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\References\Mapping\Driver; -use Gedmo\Mapping\Driver\Xml as BaseXml; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for References @@ -12,98 +19,90 @@ * extension. * * @author Aram Alipoor - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ class Xml extends BaseXml { /** - * @var array + * @var string[] */ - private $validTypes = array( + private const VALID_TYPES = [ 'document', - 'entity' - ); + 'entity', + ]; /** - * @var array + * @var string[] */ - private $validReferences = array( + private array $validReferences = [ 'referenceOne', 'referenceMany', - 'referenceManyEmbed' - ); + 'referenceManyEmbed', + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { /** - * @var \SimpleXmlElement $xml + * @var \SimpleXmlElement */ - $xml = $this->_getMapping($meta->name); + $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); - if ($xmlDoctrine->getName() === 'entity' || $xmlDoctrine->getName() === 'document' || $xmlDoctrine->getName() === 'mapped-superclass') { + if (in_array($xmlDoctrine->getName(), ['mapped-superclass', 'entity', 'document'], true)) { if (isset($xml->reference)) { /** - * @var \SimpleXMLElement $element + * @var \SimpleXMLElement */ foreach ($xml->reference as $element) { if (!$this->_isAttributeSet($element, 'type')) { - throw new InvalidMappingException("Reference type (document or entity) is not set in class - {$meta->name}"); + throw new InvalidMappingException("Reference type (document or entity) is not set in class - {$meta->getName()}"); } $type = $this->_getAttribute($element, 'type'); - if (!in_array($type, $this->validTypes)) { - throw new InvalidMappingException( - $type . - ' is not a valid reference type, valid types are: ' . - implode(', ', $this->validTypes) - ); + if (!in_array($type, self::VALID_TYPES, true)) { + throw new InvalidMappingException($type.' is not a valid reference type, valid types are: '.implode(', ', self::VALID_TYPES)); } $reference = $this->_getAttribute($element, 'reference'); - if (!in_array($reference, $this->validReferences)) { - throw new InvalidMappingException( - $reference . - ' is not a valid reference, valid references are: ' . - implode(', ', $this->validReferences) - ); + if (!in_array($reference, $this->validReferences, true)) { + throw new InvalidMappingException($reference.' is not a valid reference, valid references are: '.implode(', ', $this->validReferences)); } if (!$this->_isAttributeSet($element, 'field')) { - throw new InvalidMappingException("Reference field is not set in class - {$meta->name}"); + throw new InvalidMappingException("Reference field is not set in class - {$meta->getName()}"); } $field = $this->_getAttribute($element, 'field'); if (!$this->_isAttributeSet($element, 'class')) { - throw new InvalidMappingException("Reference field is not set in class - {$meta->name}"); + throw new InvalidMappingException("Reference field is not set in class - {$meta->getName()}"); } $class = $this->_getAttribute($element, 'class'); if (!$this->_isAttributeSet($element, 'identifier')) { - throw new InvalidMappingException("Reference identifier is not set in class - {$meta->name}"); + throw new InvalidMappingException("Reference identifier is not set in class - {$meta->getName()}"); } $identifier = $this->_getAttribute($element, 'identifier'); - $config[$reference][$field] = array( + $config[$reference][$field] = [ 'field' => $field, 'type' => $type, 'class' => $class, - 'identifier' => $identifier - ); + 'identifier' => $identifier, + ]; - if (!$this->_isAttributeSet($element, 'mappedBy')) { + if ($this->_isAttributeSet($element, 'mappedBy')) { $config[$reference][$field]['mappedBy'] = $this->_getAttribute($element, 'mappedBy'); } - if (!$this->_isAttributeSet($element, 'inversedBy')) { + if ($this->_isAttributeSet($element, 'inversedBy')) { $config[$reference][$field]['inversedBy'] = $this->_getAttribute($element, 'inversedBy'); } } } } + + return $config; } } diff --git a/lib/Gedmo/References/Mapping/Driver/Yaml.php b/src/References/Mapping/Driver/Yaml.php similarity index 58% rename from lib/Gedmo/References/Mapping/Driver/Yaml.php rename to src/References/Mapping/Driver/Yaml.php index 37bccd8cca..48a8b4bb10 100644 --- a/lib/Gedmo/References/Mapping/Driver/Yaml.php +++ b/src/References/Mapping/Driver/Yaml.php @@ -1,62 +1,67 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\References\Mapping\Driver; -use Gedmo\Mapping\Driver\File; -use Gedmo\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; /** * @author Gonzalo Vilaseca + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ class Yaml extends File implements Driver { /** * File extension + * * @var string */ protected $_extension = '.dcm.yml'; - private $validReferences = array( - 'referenceOne' => array(), - 'referenceMany' => array(), - 'referenceManyEmbed' => array(), - ); - /** - * {@inheritDoc} + * @var array>> */ + private array $validReferences = [ + 'referenceOne' => [], + 'referenceMany' => [], + 'referenceManyEmbed' => [], + ]; + public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); - - if (isset($mapping['gedmo']) && isset($mapping['gedmo']['reference'])) { + $mapping = $this->_getMapping($meta->getName()); + if (isset($mapping['gedmo'], $mapping['gedmo']['reference'])) { foreach ($mapping['gedmo']['reference'] as $field => $fieldMapping) { $reference = $fieldMapping['reference']; - if (!in_array($reference, array_keys($this->validReferences))) { - throw new InvalidMappingException( - $reference . - ' is not a valid reference, valid references are: ' . - implode(', ', array_keys($this->validReferences)) - ); + if (!in_array($reference, array_keys($this->validReferences), true)) { + throw new InvalidMappingException($reference.' is not a valid reference, valid references are: '.implode(', ', array_keys($this->validReferences))); } - $config[$reference][$field] = array( + $config[$reference][$field] = [ 'field' => $field, - 'type' => $fieldMapping['type'], + 'type' => $fieldMapping['type'], 'class' => $fieldMapping['class'], - ); + ]; if (array_key_exists('mappedBy', $fieldMapping)) { $config[$reference][$field]['mappedBy'] = $fieldMapping['mappedBy']; - } if (array_key_exists('identifier', $fieldMapping)) { $config[$reference][$field]['identifier'] = $fieldMapping['identifier']; - } if (array_key_exists('inversedBy', $fieldMapping)) { @@ -65,11 +70,10 @@ public function readExtendedMetadata($meta, array &$config) } } $config = array_merge($this->validReferences, $config); + + return $config; } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse($file); diff --git a/lib/Gedmo/References/Mapping/Event/Adapter/ODM.php b/src/References/Mapping/Event/Adapter/ODM.php similarity index 64% rename from lib/Gedmo/References/Mapping/Event/Adapter/ODM.php rename to src/References/Mapping/Event/Adapter/ODM.php index 23753cbc3a..4b24e7ac31 100644 --- a/lib/Gedmo/References/Mapping/Event/Adapter/ODM.php +++ b/src/References/Mapping/Event/Adapter/ODM.php @@ -1,13 +1,20 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\References\Mapping\Event\Adapter; use Doctrine\ODM\MongoDB\DocumentManager; -use Doctrine\ODM\MongoDB\Proxy\Proxy as MongoDBProxy; -use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Proxy\Proxy as ORMProxy; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\Proxy as PersistenceProxy; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\References\Mapping\Event\ReferencesAdapter; +use ProxyManager\Proxy\GhostObjectInterface; /** * Doctrine event adapter for ODM references behavior @@ -15,27 +22,23 @@ * @author Gediminas Morkevicius * @author Bulat Shakirzyanov * @author Jonathan H. Wage - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ODM extends BaseAdapterODM implements ReferencesAdapter { - /** - * @inheritDoc - */ public function getIdentifier($om, $object, $single = true) { if ($om instanceof DocumentManager) { return $this->extractIdentifier($om, $object, $single); } - if ($om instanceof EntityManager) { - if ($object instanceof ORMProxy) { + if ($om instanceof EntityManagerInterface) { + if ($object instanceof PersistenceProxy) { $id = $om->getUnitOfWork()->getEntityIdentifier($object); } else { $meta = $om->getClassMetadata(get_class($object)); - $id = array(); - foreach ($meta->identifier as $name) { - $id[$name] = $meta->getReflectionProperty($name)->getValue($object); + $id = []; + foreach ($meta->getIdentifier() as $name) { + $id[$name] = $meta->getFieldValue($object, $name); // return null if one of identifiers is missing if (!$id[$name]) { return null; @@ -49,14 +52,12 @@ public function getIdentifier($om, $object, $single = true) return $id; } + + return null; } - /** - * @inheritDoc - */ public function getSingleReference($om, $class, $identifier) { - $this->throwIfNotEntityManager($om); $meta = $om->getClassMetadata($class); if (!$meta->isInheritanceTypeNone()) { @@ -66,29 +67,19 @@ public function getSingleReference($om, $class, $identifier) return $om->getReference($class, $identifier); } - /** - * @inheritDoc - */ public function extractIdentifier($om, $object, $single = true) { $meta = $om->getClassMetadata(get_class($object)); - if ($object instanceof MongoDBProxy) { + if ($object instanceof GhostObjectInterface) { $id = $om->getUnitOfWork()->getDocumentIdentifier($object); } else { - $id = $meta->getReflectionProperty($meta->identifier)->getValue($object); + $id = $meta->getFieldValue($object, $meta->getIdentifier()[0]); } if ($single || !$id) { return $id; - } else { - return array($meta->identifier => $id); } - } - /** - * Override so we don't get an exception. We want to allow this. - */ - private function throwIfNotEntityManager(EntityManager $em) - { + return [$meta->getIdentifier()[0] => $id]; } } diff --git a/lib/Gedmo/References/Mapping/Event/Adapter/ORM.php b/src/References/Mapping/Event/Adapter/ORM.php similarity index 62% rename from lib/Gedmo/References/Mapping/Event/Adapter/ORM.php rename to src/References/Mapping/Event/Adapter/ORM.php index 3427550d18..294fb09832 100644 --- a/lib/Gedmo/References/Mapping/Event/Adapter/ORM.php +++ b/src/References/Mapping/Event/Adapter/ORM.php @@ -1,15 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\References\Mapping\Event\Adapter; use Doctrine\ODM\MongoDB\DocumentManager as MongoDocumentManager; -use Doctrine\ODM\MongoDB\Proxy\Proxy as MongoDBProxy; use Doctrine\ODM\PHPCR\DocumentManager as PhpcrDocumentManager; -use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Proxy\Proxy as ORMProxy; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\Proxy as PersistenceProxy; use Gedmo\Exception\InvalidArgumentException; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\References\Mapping\Event\ReferencesAdapter; +use ProxyManager\Proxy\GhostObjectInterface; /** * Doctrine event adapter for ORM references behavior @@ -17,49 +24,45 @@ * @author Gediminas Morkevicius * @author Bulat Shakirzyanov * @author Jonathan H. Wage - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ORM extends BaseAdapterORM implements ReferencesAdapter { - /** - * @inheritDoc - */ public function getIdentifier($om, $object, $single = true) { - if ($om instanceof EntityManager) { + if ($om instanceof EntityManagerInterface) { return $this->extractIdentifier($om, $object, $single); } if ($om instanceof MongoDocumentManager) { $meta = $om->getClassMetadata(get_class($object)); - if ($object instanceof MongoDBProxy) { + if ($object instanceof GhostObjectInterface) { $id = $om->getUnitOfWork()->getDocumentIdentifier($object); } else { - $id = $meta->getReflectionProperty($meta->identifier)->getValue($object); + $id = $meta->getFieldValue($object, $meta->getIdentifier()[0]); } if ($single || !$id) { return $id; } - return array($meta->identifier => $id); + return [$meta->getIdentifier()[0] => $id]; } if ($om instanceof PhpcrDocumentManager) { $meta = $om->getClassMetadata(get_class($object)); - $id = $meta->getReflectionProperty($meta->identifier)->getValue($object); + assert(1 === count($meta->getIdentifier())); + $id = $meta->getFieldValue($object, $meta->getIdentifier()[0]); if ($single || !$id) { return $id; } - return array($meta->identifier => $id); + return [$meta->getIdentifier()[0] => $id]; } + + return null; } - /** - * @inheritDoc - */ public function getSingleReference($om, $class, $identifier) { $this->throwIfNotDocumentManager($om); @@ -74,18 +77,15 @@ public function getSingleReference($om, $class, $identifier) return $om->getReference($class, $identifier); } - /** - * @inheritDoc - */ public function extractIdentifier($om, $object, $single = true) { - if ($object instanceof ORMProxy) { + if ($object instanceof PersistenceProxy) { $id = $om->getUnitOfWork()->getEntityIdentifier($object); } else { $meta = $om->getClassMetadata(get_class($object)); - $id = array(); - foreach ($meta->identifier as $name) { - $id[$name] = $meta->getReflectionProperty($name)->getValue($object); + $id = []; + foreach ($meta->getIdentifier() as $name) { + $id[$name] = $meta->getFieldValue($object, $name); // return null if one of identifiers is missing if (!$id[$name]) { return null; @@ -102,18 +102,15 @@ public function extractIdentifier($om, $object, $single = true) /** * Override so we don't get an exception. We want to allow this. + * + * @param mixed $dm + * + * @phpstan-assert MongoDocumentManager|PhpcrDocumentManager $dm */ - private function throwIfNotDocumentManager($dm) + private function throwIfNotDocumentManager($dm): void { if (!($dm instanceof MongoDocumentManager) && !($dm instanceof PhpcrDocumentManager)) { - throw new InvalidArgumentException( - sprintf( - 'Expected a %s or %s instance but got "%s"', - 'Doctrine\ODM\MongoDB\DocumentManager', - 'Doctrine\ODM\PHPCR\DocumentManager', - is_object($dm) ? get_class($dm) : gettype($dm) - ) - ); + throw new InvalidArgumentException(sprintf('Expected a %s or %s instance but got "%s"', MongoDocumentManager::class, 'Doctrine\ODM\PHPCR\DocumentManager', is_object($dm) ? get_class($dm) : gettype($dm))); } } } diff --git a/src/References/Mapping/Event/ReferencesAdapter.php b/src/References/Mapping/Event/ReferencesAdapter.php new file mode 100644 index 0000000000..df5fdbaad2 --- /dev/null +++ b/src/References/Mapping/Event/ReferencesAdapter.php @@ -0,0 +1,58 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\References\Mapping\Event; + +use Doctrine\Persistence\ObjectManager; +use Gedmo\Mapping\Event\AdapterInterface; + +/** + * Doctrine event adapter for the References extension. + * + * @author Gediminas Morkevicius + * @author Bulat Shakirzyanov + * @author Jonathan H. Wage + */ +interface ReferencesAdapter extends AdapterInterface +{ + /** + * Gets the identifier of the given object using the provided object manager. + * + * @param ObjectManager $om + * @param object $object + * @param bool $single + * + * @return array|string|int|null array or single identifier + */ + public function getIdentifier($om, $object, $single = true); + + /** + * Gets a single reference from the provided object manager for a class and identifier. + * + * @param ObjectManager $om + * @param string $class + * @param array|string|int $identifier + * + * @phpstan-param class-string $class + * + * @return object|null + */ + public function getSingleReference($om, $class, $identifier); + + /** + * Extracts identifiers from an object or proxy using the provided object manager. + * + * @param ObjectManager $om + * @param object $object + * @param bool $single + * + * @return array|string|int|null array or single identifier + */ + public function extractIdentifier($om, $object, $single = true); +} diff --git a/lib/Gedmo/References/ReferencesListener.php b/src/References/ReferencesListener.php similarity index 59% rename from lib/Gedmo/References/ReferencesListener.php rename to src/References/ReferencesListener.php index b6eca05ab7..0df295e342 100644 --- a/lib/Gedmo/References/ReferencesListener.php +++ b/src/References/ReferencesListener.php @@ -1,11 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\References; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\EventArgs; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\MappedEventSubscriber; +use Gedmo\References\Mapping\Event\ReferencesAdapter; /** * Listener for loading and persisting cross database references. @@ -13,37 +24,80 @@ * @author Gediminas Morkevicius * @author Bulat Shakirzyanov * @author Jonathan H. Wage - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @phpstan-type ReferenceConfiguration = array{ + * field?: string, + * type?: string, + * class?: class-string, + * identifier?: string, + * mappedBy?: string, + * inversedBy?: string, + * } + * @phpstan-type ReferencesConfiguration = array{ + * referenceMany?: array, + * referenceManyEmbed?: array, + * referenceOne?: array, + * useObjectClass?: class-string, + * } + * + * @phpstan-extends MappedEventSubscriber + * + * @final since gedmo/doctrine-extensions 3.11 */ class ReferencesListener extends MappedEventSubscriber { - private $managers; + /** + * @var array + */ + private array $managers; - public function __construct(array $managers = array()) + /** + * @param array $managers + */ + public function __construct(array $managers = []) { + parent::__construct(); + $this->managers = $managers; } + /** + * @param LoadClassMetadataEventArgs $eventArgs + * + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $eventArgs + * + * @return void + */ public function loadClassMetadata(EventArgs $eventArgs) { - $ea = $this->getEventAdapter($eventArgs); $this->loadMetadataForObjectClass( - $ea->getObjectManager(), $eventArgs->getClassMetadata() + $eventArgs->getObjectManager(), $eventArgs->getClassMetadata() ); } + /** + * @param LifecycleEventArgs $eventArgs + * + * @phpstan-param LifecycleEventArgs $eventArgs + * + * @return void + */ public function postLoad(EventArgs $eventArgs) { $ea = $this->getEventAdapter($eventArgs); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - $config = $this->getConfiguration($om, $meta->name); + $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['referenceOne'])) { foreach ($config['referenceOne'] as $mapping) { $property = $meta->reflClass->getProperty($mapping['field']); - $property->setAccessible(true); + + if (PHP_VERSION_ID < 80100) { + $property->setAccessible(true); + } + if (isset($mapping['identifier'])) { $referencedObjectId = $meta->getFieldValue($object, $mapping['identifier']); if (null !== $referencedObjectId) { @@ -63,28 +117,29 @@ public function postLoad(EventArgs $eventArgs) if (isset($config['referenceMany'])) { foreach ($config['referenceMany'] as $mapping) { $property = $meta->reflClass->getProperty($mapping['field']); - $property->setAccessible(true); + + if (PHP_VERSION_ID < 80100) { + $property->setAccessible(true); + } + if (isset($mapping['mappedBy'])) { $id = $ea->extractIdentifier($om, $object); $manager = $this->getManager($mapping['type']); $class = $mapping['class']; $refMeta = $manager->getClassMetadata($class); - $refConfig = $this->getConfiguration($manager, $refMeta->name); + $refConfig = $this->getConfiguration($manager, $refMeta->getName()); if (isset($refConfig['referenceOne'][$mapping['mappedBy']])) { $refMapping = $refConfig['referenceOne'][$mapping['mappedBy']]; $identifier = $refMapping['identifier']; $property->setValue( $object, new LazyCollection( - function () use ($id, &$manager, $class, $identifier) { - $results = $manager - ->getRepository($class) - ->findBy(array( + static fn () => new ArrayCollection( + $manager->getRepository($class) + ->findBy([ $identifier => $id, - )); - - return new ArrayCollection((is_array($results) ? $results : $results->toArray())); - } + ]) + ) ) ); } @@ -95,26 +150,49 @@ function () use ($id, &$manager, $class, $identifier) { $this->updateManyEmbedReferences($eventArgs); } + /** + * @param LifecycleEventArgs $eventArgs + * + * @phpstan-param LifecycleEventArgs $eventArgs + * + * @return void + */ public function prePersist(EventArgs $eventArgs) { $this->updateReferences($eventArgs); } + /** + * @param LifecycleEventArgs $eventArgs + * + * @phpstan-param LifecycleEventArgs $eventArgs + * + * @return void + */ public function preUpdate(EventArgs $eventArgs) { $this->updateReferences($eventArgs); } + /** + * @return string[] + */ public function getSubscribedEvents() { - return array( + return [ 'postLoad', 'loadClassMetadata', 'prePersist', 'preUpdate', - ); + ]; } + /** + * @param string $type + * @param ObjectManager $manager + * + * @return void + */ public function registerManager($type, $manager) { $this->managers[$type] = $manager; @@ -130,24 +208,80 @@ public function getManager($type) return $this->managers[$type]; } + /** + * @param LifecycleEventArgs $eventArgs + * + * @phpstan-param LifecycleEventArgs $eventArgs + * + * @return void + */ + public function updateManyEmbedReferences(EventArgs $eventArgs) + { + $ea = $this->getEventAdapter($eventArgs); + $om = $ea->getObjectManager(); + $object = $ea->getObject(); + $meta = $om->getClassMetadata(get_class($object)); + $config = $this->getConfiguration($om, $meta->getName()); + + if (isset($config['referenceManyEmbed'])) { + foreach ($config['referenceManyEmbed'] as $mapping) { + $property = $meta->reflClass->getProperty($mapping['field']); + + if (PHP_VERSION_ID < 80100) { + $property->setAccessible(true); + } + + $id = $ea->extractIdentifier($om, $object); + $manager = $this->getManager('document'); + + $class = $mapping['class']; + $refMeta = $manager->getClassMetadata($class); + // Trigger the loading of the configuration to validate the mapping + $this->getConfiguration($manager, $refMeta->getName()); + + $identifier = $mapping['identifier']; + $property->setValue( + $object, + new LazyCollection( + static fn () => new ArrayCollection( + $manager->getRepository($class) + ->findBy([ + $identifier => $id, + ]) + ) + ) + ); + } + } + } + protected function getNamespace() { return __NAMESPACE__; } - private function updateReferences(EventArgs $eventArgs) + /** + * @param LifecycleEventArgs $eventArgs + * + * @phpstan-param LifecycleEventArgs $eventArgs + */ + private function updateReferences(EventArgs $eventArgs): void { $ea = $this->getEventAdapter($eventArgs); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - $config = $this->getConfiguration($om, $meta->name); + $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['referenceOne'])) { foreach ($config['referenceOne'] as $mapping) { if (isset($mapping['identifier'])) { $property = $meta->reflClass->getProperty($mapping['field']); - $property->setAccessible(true); + + if (PHP_VERSION_ID < 80100) { + $property->setAccessible(true); + } + $referencedObject = $property->getValue($object); if (is_object($referencedObject)) { @@ -166,44 +300,4 @@ private function updateReferences(EventArgs $eventArgs) $this->updateManyEmbedReferences($eventArgs); } - - public function updateManyEmbedReferences(EventArgs $eventArgs) - { - $ea = $this->getEventAdapter($eventArgs); - $om = $ea->getObjectManager(); - $object = $ea->getObject(); - $meta = $om->getClassMetadata(get_class($object)); - $config = $this->getConfiguration($om, $meta->name); - - if (isset($config['referenceManyEmbed'])) { - foreach ($config['referenceManyEmbed'] as $mapping) { - $property = $meta->reflClass->getProperty($mapping['field']); - $property->setAccessible(true); - - $id = $ea->extractIdentifier($om, $object); - $manager = $this->getManager('document'); - - $class = $mapping['class']; - $refMeta = $manager->getClassMetadata($class); - // Trigger the loading of the configuration to validate the mapping - $this->getConfiguration($manager, $refMeta->name); - - $identifier = $mapping['identifier']; - $property->setValue( - $object, - new LazyCollection( - function () use ($id, &$manager, $class, $identifier) { - $results = $manager - ->getRepository($class) - ->findBy(array( - $identifier => $id, - )); - - return new ArrayCollection((is_array($results) ? $results : $results->toArray())); - } - ) - ); - } - } - } } diff --git a/lib/Gedmo/Sluggable/Handler/InversedRelativeSlugHandler.php b/src/Sluggable/Handler/InversedRelativeSlugHandler.php similarity index 66% rename from lib/Gedmo/Sluggable/Handler/InversedRelativeSlugHandler.php rename to src/Sluggable/Handler/InversedRelativeSlugHandler.php index e32ffe9bef..3eea7e29d6 100644 --- a/lib/Gedmo/Sluggable/Handler/InversedRelativeSlugHandler.php +++ b/src/Sluggable/Handler/InversedRelativeSlugHandler.php @@ -1,22 +1,31 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sluggable\Handler; -use Doctrine\Common\Persistence\ObjectManager; -use Gedmo\Sluggable\SluggableListener; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Doctrine\Persistence\Proxy; +use Gedmo\Exception\InvalidMappingException; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; +use Gedmo\Sluggable\SluggableListener; use Gedmo\Tool\Wrapper\AbstractWrapper; -use Gedmo\Exception\InvalidMappingException; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; /** -* Sluggable handler which should be used for inversed relation mapping -* used together with RelativeSlugHandler. Updates back related slug on -* relation changes -* -* @author Gediminas Morkevicius -* @license MIT License (http://www.opensource.org/licenses/mit-license.php) -*/ + * Sluggable handler which should be used for inversed relation mapping + * used together with RelativeSlugHandler. Updates back related slug on + * relation changes + * + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ class InversedRelativeSlugHandler implements SlugHandlerInterface { /** @@ -29,58 +38,41 @@ class InversedRelativeSlugHandler implements SlugHandlerInterface */ protected $sluggable; - /** - * $options = array( - * 'relationClass' => 'objectclass', - * 'inverseSlugField' => 'slug', - * 'mappedBy' => 'relationField' - * ) - * {@inheritDoc} - */ public function __construct(SluggableListener $sluggable) { $this->sluggable = $sluggable; } - /** - * {@inheritDoc} - */ public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug) { } - /** - * {@inheritDoc} - */ public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug) { } /** - * {@inheritDoc} + * @param ClassMetadata $meta */ public static function validate(array $options, ClassMetadata $meta) { if (!isset($options['relationClass']) || !strlen($options['relationClass'])) { - throw new InvalidMappingException("'relationClass' option must be specified for object slug mapping - {$meta->name}"); + throw new InvalidMappingException("'relationClass' option must be specified for object slug mapping - {$meta->getName()}"); } if (!isset($options['mappedBy']) || !strlen($options['mappedBy'])) { - throw new InvalidMappingException("'mappedBy' option must be specified for object slug mapping - {$meta->name}"); + throw new InvalidMappingException("'mappedBy' option must be specified for object slug mapping - {$meta->getName()}"); } if (!isset($options['inverseSlugField']) || !strlen($options['inverseSlugField'])) { - throw new InvalidMappingException("'inverseSlugField' option must be specified for object slug mapping - {$meta->name}"); + throw new InvalidMappingException("'inverseSlugField' option must be specified for object slug mapping - {$meta->getName()}"); } } - /** - * {@inheritDoc} - */ public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug) { $this->om = $ea->getObjectManager(); $isInsert = $this->om->getUnitOfWork()->isScheduledForInsert($object); if (!$isInsert) { - $options = $config['handlers'][get_called_class()]; + $options = $config['handlers'][static::class]; $wrapped = AbstractWrapper::wrap($object, $this->om); $oldSlug = $wrapped->getPropertyValue($config['slug']); $mappedByConfig = $this->sluggable->getConfiguration( @@ -88,12 +80,14 @@ public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, $options['relationClass'] ); if ($mappedByConfig) { + assert(class_exists($options['relationClass'])); + $meta = $this->om->getClassMetadata($options['relationClass']); if (!$meta->isSingleValuedAssociation($options['mappedBy'])) { - throw new InvalidMappingException("Unable to find ".$wrapped->getMetadata()->name." relation - [{$options['mappedBy']}] in class - {$meta->name}"); + throw new InvalidMappingException('Unable to find '.$wrapped->getMetadata()->getName()." relation - [{$options['mappedBy']}] in class - {$meta->getName()}"); } if (!isset($mappedByConfig['slugs'][$options['inverseSlugField']])) { - throw new InvalidMappingException("Unable to find slug field - [{$options['inverseSlugField']}] in class - {$meta->name}"); + throw new InvalidMappingException("Unable to find slug field - [{$options['inverseSlugField']}] in class - {$meta->getName()}"); } $mappedByConfig['slug'] = $mappedByConfig['slugs'][$options['inverseSlugField']]['slug']; $mappedByConfig['mappedBy'] = $options['mappedBy']; @@ -106,15 +100,16 @@ public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, continue; } foreach ($objects as $object) { - if (property_exists($object, '__isInitialized__') && !$object->__isInitialized__) { + // @todo: Remove the check against `method_exists()` in the next major release. + if (($object instanceof Proxy || method_exists($object, '__isInitialized')) && !$object->__isInitialized()) { continue; } - $oid = spl_object_hash($object); - $objectSlug = $meta->getReflectionProperty($mappedByConfig['slug'])->getValue($object); + + $objectSlug = (string) $meta->getFieldValue($object, $mappedByConfig['slug']); if (preg_match("@^{$oldSlug}@smi", $objectSlug)) { $objectSlug = str_replace($oldSlug, $slug, $objectSlug); - $meta->getReflectionProperty($mappedByConfig['slug'])->setValue($object, $objectSlug); - $ea->setOriginalObjectProperty($uow, $oid, $mappedByConfig['slug'], $objectSlug); + $meta->setFieldValue($object, $mappedByConfig['slug'], $objectSlug); + $ea->setOriginalObjectProperty($uow, $object, $mappedByConfig['slug'], $objectSlug); } } } @@ -122,9 +117,6 @@ public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, } } - /** - * {@inheritDoc} - */ public function handlesUrlization() { return false; diff --git a/lib/Gedmo/Sluggable/Handler/RelativeSlugHandler.php b/src/Sluggable/Handler/RelativeSlugHandler.php similarity index 68% rename from lib/Gedmo/Sluggable/Handler/RelativeSlugHandler.php rename to src/Sluggable/Handler/RelativeSlugHandler.php index 8077855933..2fdac40b9a 100644 --- a/lib/Gedmo/Sluggable/Handler/RelativeSlugHandler.php +++ b/src/Sluggable/Handler/RelativeSlugHandler.php @@ -1,26 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sluggable\Handler; -use Doctrine\Common\Persistence\ObjectManager; -use Gedmo\Sluggable\SluggableListener; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\InvalidMappingException; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; +use Gedmo\Sluggable\SluggableListener; use Gedmo\Tool\Wrapper\AbstractWrapper; -use Gedmo\Exception\InvalidMappingException; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; /** -* Sluggable handler which should be used in order to prefix -* a slug of related object. For instance user may belong to a company -* in this case user slug could look like 'company-name/user-firstname' -* where path separator separates the relative slug -* -* @author Gediminas Morkevicius -* @license MIT License (http://www.opensource.org/licenses/mit-license.php) -*/ + * Sluggable handler which should be used in order to prefix + * a slug of related object. For instance user may belong to a company + * in this case user slug could look like 'company-name/user-firstname' + * where path separator separates the relative slug + * + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ class RelativeSlugHandler implements SlugHandlerInterface { - const SEPARATOR = '/'; + public const SEPARATOR = '/'; /** * @var ObjectManager @@ -35,39 +43,27 @@ class RelativeSlugHandler implements SlugHandlerInterface /** * Used options * - * @var array + * @var array */ - private $usedOptions; + private array $usedOptions = []; /** - * Callable of original transliterator - * which is used by sluggable + * Callable of original transliterator which is used by the sluggable listener. * - * @var callable + * @var callable(string, string, object): string */ private $originalTransliterator; - /** - * $options = array( - * 'separator' => '/', - * 'relationField' => 'something', - * 'relationSlugField' => 'slug' - * ) - * {@inheritDoc} - */ public function __construct(SluggableListener $sluggable) { $this->sluggable = $sluggable; } - /** - * {@inheritDoc} - */ public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug) { $this->om = $ea->getObjectManager(); $isInsert = $this->om->getUnitOfWork()->isScheduledForInsert($object); - $this->usedOptions = $config['handlers'][get_called_class()]; + $this->usedOptions = $config['handlers'][static::class]; if (!isset($this->usedOptions['separator'])) { $this->usedOptions['separator'] = self::SEPARATOR; } @@ -79,28 +75,22 @@ public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, } } - /** - * {@inheritDoc} - */ public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug) { $this->originalTransliterator = $this->sluggable->getTransliterator(); - $this->sluggable->setTransliterator(array($this, 'transliterate')); + $this->sluggable->setTransliterator([$this, 'transliterate']); } /** - * {@inheritDoc} + * @param ClassMetadata $meta */ public static function validate(array $options, ClassMetadata $meta) { if (!$meta->isSingleValuedAssociation($options['relationField'])) { - throw new InvalidMappingException("Unable to find slug relation through field - [{$options['relationField']}] in class - {$meta->name}"); + throw new InvalidMappingException("Unable to find slug relation through field - [{$options['relationField']}] in class - {$meta->getName()}"); } } - /** - * {@inheritDoc} - */ public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug) { } @@ -119,7 +109,11 @@ public function transliterate($text, $separator, $object) { $result = call_user_func_array( $this->originalTransliterator, - array($text, $separator, $object) + [$text, $separator, $object] + ); + $result = call_user_func_array( + $this->sluggable->getUrlizer(), + [$result, $separator, $object] ); $wrapped = AbstractWrapper::wrap($object, $this->om); $relation = $wrapped->getPropertyValue($this->usedOptions['relationField']); @@ -130,20 +124,18 @@ public function transliterate($text, $separator, $object) if (isset($this->usedOptions['urilize']) && $this->usedOptions['urilize']) { $slug = call_user_func_array( $this->originalTransliterator, - array($slug, $separator, $object) + [$slug, $separator, $object] ); } $result = $slug.$this->usedOptions['separator'].$result; } + $this->sluggable->setTransliterator($this->originalTransliterator); return $result; } - /** - * {@inheritDoc} - */ public function handlesUrlization() { return true; diff --git a/src/Sluggable/Handler/SlugHandlerInterface.php b/src/Sluggable/Handler/SlugHandlerInterface.php new file mode 100644 index 0000000000..455a38c4c5 --- /dev/null +++ b/src/Sluggable/Handler/SlugHandlerInterface.php @@ -0,0 +1,90 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sluggable\Handler; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; +use Gedmo\Sluggable\SluggableListener; + +/** + * Interface defining a handler for the sluggable behavior. + * Usage is intended only for internal access of the + * Sluggable extension and should not be used elsewhere. + * + * @author Gediminas Morkevicius + * + * @phpstan-import-type SlugConfiguration from SluggableListener + */ +interface SlugHandlerInterface +{ + /** + * Create a new handler instance + */ + public function __construct(SluggableListener $sluggable); + + /** + * Hook on slug handlers before the decision is made whether + * the slug needs to be recalculated. + * + * @param array $config + * @param object $object + * @param string $slug + * @param bool $needToChangeSlug + * + * @phpstan-param SlugConfiguration $config + * + * @return void + */ + public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug); + + /** + * Hook on slug handlers called after the slug is built. + * + * @param array $config + * @param object $object + * @param string $slug + * + * @phpstan-param SlugConfiguration $config + * + * @return void + */ + public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug); + + /** + * Hook for slug handlers called after the slug is completed. + * + * @param array $config + * @param object $object + * @param string $slug + * + * @phpstan-param SlugConfiguration $config + * + * @return void + */ + public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug); + + /** + * @return bool Whether this handler has already urlized the slug + */ + public function handlesUrlization(); + + /** + * Validates the options for the handler. + * + * @param array $options + * @param ClassMetadata $meta + * + * @throws InvalidMappingException if the configuration is invalid + * + * @return void + */ + public static function validate(array $options, ClassMetadata $meta); +} diff --git a/src/Sluggable/Handler/SlugHandlerWithUniqueCallbackInterface.php b/src/Sluggable/Handler/SlugHandlerWithUniqueCallbackInterface.php new file mode 100644 index 0000000000..8645bfa0b5 --- /dev/null +++ b/src/Sluggable/Handler/SlugHandlerWithUniqueCallbackInterface.php @@ -0,0 +1,36 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sluggable\Handler; + +use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; +use Gedmo\Sluggable\SluggableListener; + +/** + * This adds the ability for a slug handler to change the slug just before its + * uniqueness is ensured. It is also called if the unique options are _not_ + * set. + * + * @author Gediminas Morkevicius + * + * @phpstan-import-type SlugConfiguration from SluggableListener + */ +interface SlugHandlerWithUniqueCallbackInterface extends SlugHandlerInterface +{ + /** + * Hook for slug handlers called before it is made unique. + * + * @param SlugConfiguration $config + * @param object $object + * @param string $slug + * + * @return void + */ + public function beforeMakingUnique(SluggableAdapter $ea, array &$config, $object, &$slug); +} diff --git a/lib/Gedmo/Sluggable/Handler/TreeSlugHandler.php b/src/Sluggable/Handler/TreeSlugHandler.php similarity index 68% rename from lib/Gedmo/Sluggable/Handler/TreeSlugHandler.php rename to src/Sluggable/Handler/TreeSlugHandler.php index 32afe68491..4798e7a042 100644 --- a/lib/Gedmo/Sluggable/Handler/TreeSlugHandler.php +++ b/src/Sluggable/Handler/TreeSlugHandler.php @@ -1,13 +1,23 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sluggable\Handler; -use Doctrine\Common\Persistence\ObjectManager; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; -use Gedmo\Sluggable\SluggableListener; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Doctrine\Persistence\Proxy; +use Gedmo\Exception\InvalidMappingException; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; +use Gedmo\Sluggable\SluggableListener; use Gedmo\Tool\Wrapper\AbstractWrapper; -use Gedmo\Exception\InvalidMappingException; + +use function Symfony\Component\String\u; /** * Sluggable handler which slugs all parent nodes @@ -15,11 +25,12 @@ * category tree slug could look like "food/fruits/apples" * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ -class TreeSlugHandler implements SlugHandlerInterface +class TreeSlugHandler implements SlugHandlerWithUniqueCallbackInterface { - const SEPARATOR = '/'; + public const SEPARATOR = '/'; /** * @var ObjectManager @@ -31,57 +42,39 @@ class TreeSlugHandler implements SlugHandlerInterface */ protected $sluggable; - /** - * @var string - */ - private $prefix; + private string $prefix = ''; - /** - * @var string - */ - private $suffix; + private string $suffix = ''; /** * True if node is being inserted - * - * @var boolean */ - private $isInsert = false; + private bool $isInsert = false; /** * Transliterated parent slug - * - * @var string */ - private $parentSlug; + private string $parentSlug = ''; /** * Used path separator - * - * @var string */ - private $usedPathSeparator; + private string $usedPathSeparator = self::SEPARATOR; - /** - * {@inheritDoc} - */ public function __construct(SluggableListener $sluggable) { $this->sluggable = $sluggable; } - /** - * {@inheritDoc} - */ public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug) { $this->om = $ea->getObjectManager(); $this->isInsert = $this->om->getUnitOfWork()->isScheduledForInsert($object); - $options = $config['handlers'][get_called_class()]; + $options = $config['handlers'][static::class]; - $this->usedPathSeparator = isset($options['separator']) ? $options['separator'] : self::SEPARATOR; - $this->prefix = isset($options['prefix']) ? $options['prefix'] : ''; - $this->suffix = isset($options['suffix']) ? $options['suffix'] : ''; + $this->usedPathSeparator = $options['separator'] ?? self::SEPARATOR; + $this->prefix = $options['prefix'] ?? ''; + $this->suffix = $options['suffix'] ?? ''; if (!$this->isInsert && !$needToChangeSlug) { $changeSet = $ea->getObjectChangeSet($this->om->getUnitOfWork(), $object); @@ -91,12 +84,9 @@ public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, } } - /** - * {@inheritDoc} - */ public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug) { - $options = $config['handlers'][get_called_class()]; + $options = $config['handlers'][static::class]; $this->parentSlug = ''; $wrapped = AbstractWrapper::wrap($object, $this->om); @@ -106,32 +96,28 @@ public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$s // if needed, remove suffix from parentSlug, so we can use it to prepend it to our slug if (isset($options['suffix'])) { - $suffix = $options['suffix']; - - if (substr($this->parentSlug, -strlen($suffix)) === $suffix) { //endsWith - $this->parentSlug = substr_replace($this->parentSlug, '', -1 * strlen($suffix)); - } + $this->parentSlug = u($this->parentSlug)->trimSuffix($options['suffix'])->toString(); } } } /** - * {@inheritDoc} + * @param ClassMetadata $meta */ public static function validate(array $options, ClassMetadata $meta) { if (!$meta->isSingleValuedAssociation($options['parentRelationField'])) { - throw new InvalidMappingException("Unable to find tree parent slug relation through field - [{$options['parentRelationField']}] in class - {$meta->name}"); + throw new InvalidMappingException("Unable to find tree parent slug relation through field - [{$options['parentRelationField']}] in class - {$meta->getName()}"); } } - /** - * {@inheritDoc} - */ - public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug) + public function beforeMakingUnique(SluggableAdapter $ea, array &$config, $object, &$slug) { $slug = $this->transliterate($slug, $config['separator'], $object); + } + public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug) + { if (!$this->isInsert) { $wrapped = AbstractWrapper::wrap($object, $this->om); $meta = $wrapped->getMetadata(); @@ -146,15 +132,16 @@ public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, continue; } foreach ($objects as $object) { - if (property_exists($object, '__isInitialized__') && !$object->__isInitialized__) { + // @todo: Remove the check against `method_exists()` in the next major release. + if (($object instanceof Proxy || method_exists($object, '__isInitialized')) && !$object->__isInitialized()) { continue; } - $oid = spl_object_hash($object); - $objectSlug = $meta->getReflectionProperty($config['slug'])->getValue($object); + + $objectSlug = (string) $meta->getFieldValue($object, $config['slug']); if (preg_match("@^{$target}{$config['pathSeparator']}@smi", $objectSlug)) { $objectSlug = str_replace($target, $slug, $objectSlug); - $meta->getReflectionProperty($config['slug'])->setValue($object, $objectSlug); - $ea->setOriginalObjectProperty($uow, $oid, $config['slug'], $objectSlug); + $meta->setFieldValue($object, $config['slug'], $objectSlug); + $ea->setOriginalObjectProperty($uow, $object, $config['slug'], $objectSlug); } } } @@ -173,7 +160,7 @@ public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, */ public function transliterate($text, $separator, $object) { - $slug = $text . $this->suffix; + $slug = $text.$this->suffix; if (strlen($this->parentSlug)) { $slug = $this->parentSlug.$this->usedPathSeparator.$slug; @@ -185,9 +172,6 @@ public function transliterate($text, $separator, $object) return $slug; } - /** - * {@inheritDoc} - */ public function handlesUrlization() { return false; diff --git a/src/Sluggable/Mapping/Driver/Annotation.php b/src/Sluggable/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..9f076d6f37 --- /dev/null +++ b/src/Sluggable/Mapping/Driver/Annotation.php @@ -0,0 +1,75 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sluggable\Mapping\Driver; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Slug; +use Gedmo\Mapping\Annotation\SlugHandler; +use Gedmo\Mapping\Annotation\SlugHandlerOption; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; +use Gedmo\Sluggable\Handler\SlugHandlerInterface; + +/** + * Mapping driver for the sluggable extension which reads extended metadata from annotations on a sluggable class. + * + * @author Gediminas Morkevicius + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ + /** + * @param ClassMetadata $meta + * + * @return array, SlugHandler[]> + */ + protected function getSlugHandlers(\ReflectionProperty $property, Slug $slug, ClassMetadata $meta): array + { + if (!is_array($slug->handlers) || [] === $slug->handlers) { + return []; + } + + $handlers = []; + + foreach ($slug->handlers as $handler) { + if (!$handler instanceof SlugHandler) { + throw new InvalidMappingException("SlugHandler: {$handler} should be instance of SlugHandler annotation in entity - {$meta->getName()}"); + } + + if (!class_exists($handler->class)) { + throw new InvalidMappingException("SlugHandler class: {$handler->class} should be a valid class name in entity - {$meta->getName()}"); + } + + /** @var class-string $class */ + $class = $handler->class; + + $handlers[$class] = []; + + foreach ($handler->options as $option) { + if (!$option instanceof SlugHandlerOption) { + throw new InvalidMappingException("SlugHandlerOption: {$option} should be instance of SlugHandlerOption annotation in entity - {$meta->getName()}"); + } + + if ('' === $option->name) { + throw new InvalidMappingException("SlugHandlerOption name: {$option->name} should be valid name in entity - {$meta->getName()}"); + } + + $handlers[$class][$option->name] = $option->value; + } + + $class::validate($handlers[$class], $meta); + } + + return $handlers; + } +} diff --git a/src/Sluggable/Mapping/Driver/Attribute.php b/src/Sluggable/Mapping/Driver/Attribute.php new file mode 100644 index 0000000000..a74e71d728 --- /dev/null +++ b/src/Sluggable/Mapping/Driver/Attribute.php @@ -0,0 +1,222 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sluggable\Mapping\Driver; + +use Doctrine\ORM\Mapping\EmbeddedClassMapping; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Slug; +use Gedmo\Mapping\Annotation\SlugHandler; +use Gedmo\Mapping\Annotation\SlugHandlerOption; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Sluggable\Handler\SlugHandlerInterface; + +/** + * Mapping driver for the sluggable extension which reads extended metadata from attributes on a sluggable class. + * + * @author Gediminas Morkevicius + * + * @internal + */ +class Attribute extends AbstractAnnotationDriver +{ + /** + * Mapping object configuring a field which should have a slug computed. + */ + public const SLUG = Slug::class; + + /** + * Mapping object configuring a slug handler for a sluggable field. + */ + public const HANDLER = SlugHandler::class; + + /** + * Mapping object configuring an option for a slug handler. + * + * @deprecated since gedmo/doctrine-extensions 3.18, will be removed in version 4.0. + */ + public const HANDLER_OPTION = SlugHandlerOption::class; + + /** + * List of types which are valid for slug and sluggable fields + * + * @var string[] + */ + protected $validTypes = [ + 'string', + 'text', + 'integer', + 'int', + 'date', + 'date_immutable', + 'datetime', + 'datetime_immutable', + 'datetimetz', + 'datetimetz_immutable', + 'citext', + 'ascii_string', + ]; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // property annotations + foreach ($class->getProperties() as $property) { + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + $config = $this->retrieveSlug($meta, $config, $property); + } + + // Embedded entity + if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) { + foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) { + /** Remove conditional when ORM 2.x is no longer supported. */ + $className = ($embeddedClassInfo instanceof EmbeddedClassMapping) ? $embeddedClassInfo->class : $embeddedClassInfo['class']; + $embeddedClass = new \ReflectionClass($className); + + foreach ($embeddedClass->getProperties() as $embeddedProperty) { + $config = $this->retrieveSlug($meta, $config, $embeddedProperty, $propertyName); + } + } + } + + return $config; + } + + /** + * @param ClassMetadata $meta + * + * @return array, SlugHandler[]> + */ + protected function getSlugHandlers(\ReflectionProperty $property, Slug $slug, ClassMetadata $meta): array + { + /** @var list|null $attributeHandlers */ + $attributeHandlers = $this->reader->getPropertyAnnotation($property, self::HANDLER); + + if (null === $attributeHandlers) { + return []; + } + + $handlers = []; + + foreach ($attributeHandlers as $handler) { + if (!class_exists($handler->class)) { + throw new InvalidMappingException("SlugHandler class: {$handler->class} should be a valid class name in entity - {$meta->getName()}"); + } + + /** @var class-string $class */ + $class = $handler->class; + + $handlers[$class] = []; + + foreach ($handler->options as $name => $value) { + $handlers[$class][$name] = $value; + } + + $class::validate($handlers[$class], $meta); + } + + return $handlers; + } + + /** + * @param ClassMetadata $meta + * @param array $config + * + * @return array> + */ + private function retrieveSlug(ClassMetadata $meta, array &$config, \ReflectionProperty $property, ?string $fieldNamePrefix = null): array + { + $fieldName = null !== $fieldNamePrefix ? ($fieldNamePrefix.'.'.$property->getName()) : $property->getName(); + + // slug property + $slug = $this->reader->getPropertyAnnotation($property, self::SLUG); + + if (null === $slug) { + return $config; + } + + assert($slug instanceof Slug); + + if (!$meta->hasField($fieldName)) { + throw new InvalidMappingException("Unable to find slug [{$fieldName}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$this->isValidField($meta, $fieldName)) { + throw new InvalidMappingException("Cannot use field - [{$fieldName}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); + } + + // process slug handlers + $handlers = $this->getSlugHandlers($property, $slug, $meta); + + // process slug fields + if ([] === $slug->fields || !is_array($slug->fields)) { + throw new InvalidMappingException("Slug must contain at least one field for slug generation in class - {$meta->getName()}"); + } + + foreach ($slug->fields as $slugField) { + $slugFieldWithPrefix = null !== $fieldNamePrefix ? ($fieldNamePrefix.'.'.$slugField) : $slugField; + + if (!$meta->hasField($slugFieldWithPrefix)) { + throw new InvalidMappingException("Unable to find slug [{$slugFieldWithPrefix}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$this->isValidField($meta, $slugFieldWithPrefix)) { + throw new InvalidMappingException("Cannot use field - [{$slugFieldWithPrefix}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); + } + } + + if ([] !== $meta->getIdentifier() && $meta->isIdentifier($fieldName) && !(bool) $slug->unique) { + throw new InvalidMappingException("Identifier field - [{$fieldName}] slug must be unique in order to maintain primary key in class - {$meta->getName()}"); + } + + if (false === $slug->unique && $slug->unique_base) { + throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'"); + } + + if (false === $slug->unique && $slug->uniqueOverTranslations) { + throw new InvalidMappingException("Slug annotation [uniqueOverTranslations] can not be set if unique is unset or 'false'"); + } + + if ($slug->unique_base && !$meta->hasField($slug->unique_base) && !$meta->hasAssociation($slug->unique_base)) { + throw new InvalidMappingException("Unable to find [{$slug->unique_base}] as mapped property in entity - {$meta->getName()}"); + } + + $sluggableFields = []; + + foreach ($slug->fields as $field) { + $sluggableFields[] = null !== $fieldNamePrefix ? ($fieldNamePrefix.'.'.$field) : $field; + } + + // set all options + $config['slugs'][$fieldName] = [ + 'fields' => $sluggableFields, + 'slug' => $fieldName, + 'style' => $slug->style, + 'dateFormat' => $slug->dateFormat, + 'updatable' => $slug->updatable, + 'unique' => $slug->unique, + 'unique_base' => $slug->unique_base, + 'separator' => $slug->separator, + 'prefix' => $slug->prefix, + 'suffix' => $slug->suffix, + 'handlers' => $handlers, + 'uniqueOverTranslations' => $slug->uniqueOverTranslations, + ]; + + return $config; + } +} diff --git a/src/Sluggable/Mapping/Driver/Xml.php b/src/Sluggable/Mapping/Driver/Xml.php new file mode 100644 index 0000000000..8f69a8d3cf --- /dev/null +++ b/src/Sluggable/Mapping/Driver/Xml.php @@ -0,0 +1,165 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sluggable\Mapping\Driver; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver\Xml as BaseXml; + +/** + * This is a xml mapping driver for Sluggable + * behavioral extension. Used for extraction of extended + * metadata from xml specifically for Sluggable + * extension. + * + * @author Gediminas Morkevicius + * @author Miha Vrhovnik + * + * @internal + */ +class Xml extends BaseXml +{ + /** + * List of types which are valid for slug and sluggable fields + * + * @var string[] + */ + private const VALID_TYPES = [ + 'string', + 'text', + 'integer', + 'int', + 'datetime', + 'citext', + ]; + + public function readExtendedMetadata($meta, array &$config) + { + /** + * @var \SimpleXmlElement + */ + $xml = $this->_getMapping($meta->getName()); + + if (isset($xml->field)) { + foreach ($xml->field as $mapping) { + $field = $this->_getAttribute($mapping, 'name'); + $config = $this->buildFieldConfiguration($meta, $field, $mapping, $config); + } + } + + if (isset($xml->{'attribute-overrides'})) { + foreach ($xml->{'attribute-overrides'}->{'attribute-override'} as $mapping) { + $field = $this->_getAttribute($mapping, 'name'); + $config = $this->buildFieldConfiguration($meta, $field, $mapping->field, $config); + } + } + + return $config; + } + + /** + * Checks if $field type is valid as Sluggable field + * + * @param ClassMetadata $meta + * @param string $field + * + * @return bool + */ + protected function isValidField($meta, $field) + { + $mapping = $meta->getFieldMapping($field); + + return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true); + } + + /** + * @param ClassMetadata $meta + * @param array $config + * + * @return array + */ + private function buildFieldConfiguration(ClassMetadata $meta, string $field, \SimpleXMLElement $mapping, array $config): array + { + /** + * @var \SimpleXmlElement + */ + $mapping = $mapping->children(self::GEDMO_NAMESPACE_URI); + + if (isset($mapping->slug)) { + /** + * @var \SimpleXmlElement + */ + $slug = $mapping->slug; + if (!$this->isValidField($meta, $field)) { + throw new InvalidMappingException("Cannot use field - [{$field}] for slug storage, type is not valid and must be 'string' in class - {$meta->getName()}"); + } + $fields = array_map('trim', explode(',', (string) $this->_getAttribute($slug, 'fields'))); + foreach ($fields as $slugField) { + if (!$meta->hasField($slugField)) { + throw new InvalidMappingException("Unable to find slug [{$slugField}] as mapped property in entity - {$meta->getName()}"); + } + if (!$this->isValidField($meta, $slugField)) { + throw new InvalidMappingException("Cannot use field - [{$slugField}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); + } + } + + $handlers = []; + if (isset($slug->handler)) { + foreach ($slug->handler as $handler) { + $class = (string) $this->_getAttribute($handler, 'class'); + $handlers[$class] = []; + foreach ($handler->{'handler-option'} as $option) { + $handlers[$class][(string) $this->_getAttribute($option, 'name')] + = (string) $this->_getAttribute($option, 'value') + ; + } + $class::validate($handlers[$class], $meta); + } + } + + // set all options + $config['slugs'][$field] = [ + 'fields' => $fields, + 'slug' => $field, + 'style' => $this->_isAttributeSet($slug, 'style') ? + $this->_getAttribute($slug, 'style') : 'default', + 'updatable' => $this->_isAttributeSet($slug, 'updatable') ? + $this->_getBooleanAttribute($slug, 'updatable') : true, + 'dateFormat' => $this->_isAttributeSet($slug, 'dateFormat') ? + $this->_getAttribute($slug, 'dateFormat') : 'Y-m-d-H:i', + 'unique' => $this->_isAttributeSet($slug, 'unique') ? + $this->_getBooleanAttribute($slug, 'unique') : true, + 'unique_base' => $this->_isAttributeSet($slug, 'unique-base') ? + $this->_getAttribute($slug, 'unique-base') : null, + 'separator' => $this->_isAttributeSet($slug, 'separator') ? + $this->_getAttribute($slug, 'separator') : '-', + 'prefix' => $this->_isAttributeSet($slug, 'prefix') ? + $this->_getAttribute($slug, 'prefix') : '', + 'suffix' => $this->_isAttributeSet($slug, 'suffix') ? + $this->_getAttribute($slug, 'suffix') : '', + 'handlers' => $handlers, + 'uniqueOverTranslations' => $this->_isAttributeSet($slug, 'uniqueOverTranslations') ? + $this->_getBooleanAttribute($slug, 'uniqueOverTranslations') : false, + ]; + if (!$meta->isMappedSuperclass && $meta->isIdentifier($field) && !$config['slugs'][$field]['unique']) { + throw new InvalidMappingException("Identifier field - [{$field}] slug must be unique in order to maintain primary key in class - {$meta->getName()}"); + } + $ubase = $config['slugs'][$field]['unique_base']; + if (false === $config['slugs'][$field]['unique'] && $ubase) { + throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'"); + } + if ($ubase && !$meta->hasField($ubase) && !$meta->hasAssociation($ubase)) { + throw new InvalidMappingException("Unable to find [{$ubase}] as mapped property in entity - {$meta->getName()}"); + } + } + + return $config; + } +} diff --git a/src/Sluggable/Mapping/Driver/Yaml.php b/src/Sluggable/Mapping/Driver/Yaml.php new file mode 100644 index 0000000000..9d96296311 --- /dev/null +++ b/src/Sluggable/Mapping/Driver/Yaml.php @@ -0,0 +1,171 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sluggable\Mapping\Driver; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; + +/** + * This is a yaml mapping driver for Sluggable + * behavioral extension. Used for extraction of extended + * metadata from yaml specifically for Sluggable + * extension. + * + * @author Gediminas Morkevicius + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal + */ +class Yaml extends File implements Driver +{ + /** + * List of types which are valid for slug and sluggable fields + * + * @var string[] + */ + private const VALID_TYPES = [ + 'string', + 'text', + 'integer', + 'int', + 'datetime', + 'citext', + ]; + + /** + * File extension + * + * @var string + */ + protected $_extension = '.dcm.yml'; + + public function readExtendedMetadata($meta, array &$config) + { + $mapping = $this->_getMapping($meta->getName()); + + if (isset($mapping['fields'])) { + foreach ($mapping['fields'] as $field => $fieldMapping) { + $config = $this->buildFieldConfiguration($field, $fieldMapping, $meta, $config); + } + } + + if (isset($mapping['attributeOverride'])) { + foreach ($mapping['attributeOverride'] as $field => $overrideMapping) { + $config = $this->buildFieldConfiguration($field, $overrideMapping, $meta, $config); + } + } + + return $config; + } + + protected function _loadMappingFile($file) + { + return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); + } + + /** + * Checks if $field type is valid as Sluggable field + * + * @param ClassMetadata $meta + * @param string $field + * + * @return bool + */ + protected function isValidField($meta, $field) + { + $mapping = $meta->getFieldMapping($field); + + return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true); + } + + /** + * @param array $fieldMapping + * @param ClassMetadata $meta + * @param array $config + * + * @return array + */ + private function buildFieldConfiguration(string $field, array $fieldMapping, ClassMetadata $meta, array $config): array + { + if (isset($fieldMapping['gedmo'])) { + if (isset($fieldMapping['gedmo']['slug'])) { + $slug = $fieldMapping['gedmo']['slug']; + if (!$this->isValidField($meta, $field)) { + throw new InvalidMappingException("Cannot use field - [{$field}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); + } + // process slug handlers + $handlers = []; + if (isset($slug['handlers'])) { + foreach ($slug['handlers'] as $handlerClass => $options) { + if (!strlen($handlerClass)) { + throw new InvalidMappingException("SlugHandler class: {$handlerClass} should be a valid class name in entity - {$meta->getName()}"); + } + $handlers[$handlerClass] = $options; + $handlerClass::validate($handlers[$handlerClass], $meta); + } + } + // process slug fields + if (empty($slug['fields']) || !is_array($slug['fields'])) { + throw new InvalidMappingException("Slug must contain at least one field for slug generation in class - {$meta->getName()}"); + } + foreach ($slug['fields'] as $slugField) { + if (!$meta->hasField($slugField)) { + throw new InvalidMappingException("Unable to find slug [{$slugField}] as mapped property in entity - {$meta->getName()}"); + } + if (!$this->isValidField($meta, $slugField)) { + throw new InvalidMappingException("Cannot use field - [{$slugField}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); + } + } + + $config['slugs'][$field]['fields'] = $slug['fields']; + $config['slugs'][$field]['handlers'] = $handlers; + $config['slugs'][$field]['slug'] = $field; + $config['slugs'][$field]['style'] = isset($slug['style']) ? + (string) $slug['style'] : 'default'; + + $config['slugs'][$field]['dateFormat'] = isset($slug['dateFormat']) ? + (string) $slug['dateFormat'] : 'Y-m-d-H:i'; + + $config['slugs'][$field]['updatable'] = isset($slug['updatable']) ? + (bool) $slug['updatable'] : true; + + $config['slugs'][$field]['unique'] = isset($slug['unique']) ? + (bool) $slug['unique'] : true; + + $config['slugs'][$field]['unique_base'] = $slug['unique_base'] ?? null; + + $config['slugs'][$field]['separator'] = isset($slug['separator']) ? + (string) $slug['separator'] : '-'; + + $config['slugs'][$field]['prefix'] = isset($slug['prefix']) ? + (string) $slug['prefix'] : ''; + + $config['slugs'][$field]['suffix'] = isset($slug['suffix']) ? + (string) $slug['suffix'] : ''; + + if (!$meta->isMappedSuperclass && $meta->isIdentifier($field) && !$config['slugs'][$field]['unique']) { + throw new InvalidMappingException("Identifier field - [{$field}] slug must be unique in order to maintain primary key in class - {$meta->getName()}"); + } + $ubase = $config['slugs'][$field]['unique_base']; + if (false === $config['slugs'][$field]['unique'] && $ubase) { + throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'"); + } + if ($ubase && !$meta->hasField($ubase) && !$meta->hasAssociation($ubase)) { + throw new InvalidMappingException("Unable to find [{$ubase}] as mapped property in entity - {$meta->getName()}"); + } + } + } + + return $config; + } +} diff --git a/lib/Gedmo/Sluggable/Mapping/Event/Adapter/ODM.php b/src/Sluggable/Mapping/Event/Adapter/ODM.php similarity index 57% rename from lib/Gedmo/Sluggable/Mapping/Event/Adapter/ODM.php rename to src/Sluggable/Mapping/Event/Adapter/ODM.php index f4b12c515f..9f60cf06bc 100644 --- a/lib/Gedmo/Sluggable/Mapping/Event/Adapter/ODM.php +++ b/src/Sluggable/Mapping/Event/Adapter/ODM.php @@ -1,33 +1,36 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sluggable\Mapping\Event\Adapter; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; -use Doctrine\ODM\MongoDB\Cursor; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; use Gedmo\Tool\Wrapper\AbstractWrapper; +use MongoDB\BSON\Regex; /** * Doctrine event adapter for ODM adapted * for sluggable behavior * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ODM extends BaseAdapterODM implements SluggableAdapter { - /** - * {@inheritDoc} - */ public function getSimilarSlugs($object, $meta, array $config, $slug) { $dm = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $dm); $qb = $dm->createQueryBuilder($config['useObjectClass']); if (($identifier = $wrapped->getIdentifier()) && !$meta->isIdentifier($config['slug'])) { - $qb->field($meta->identifier)->notEqual($identifier); + $qb->field($meta->getIdentifier()[0])->notEqual($identifier); } - $qb->field($config['slug'])->equals(new \MongoRegex('/^'.preg_quote($slug, '/').'/')); + $qb->field($config['slug'])->equals(new Regex('^'.preg_quote($slug, '/'))); // use the unique_base to restrict the uniqueness check if ($config['unique'] && isset($config['unique_base'])) { @@ -43,19 +46,14 @@ public function getSimilarSlugs($object, $meta, array $config, $slug) $q = $qb->getQuery(); $q->setHydrate(false); - $result = $q->execute(); - if ($result instanceof Cursor) { - $result = $result->toArray(); - } - - return $result; + return $q->getIterator()->toArray(); } /** * This query can cause some data integrity failures since it does not * execute automatically * - * {@inheritDoc} + * {@inheritdoc} */ public function replaceRelative($object, array $config, $target, $replacement) { @@ -70,28 +68,30 @@ public function replaceRelative($object, array $config, $target, $replacement) ->getQuery() ; $q->setHydrate(false); - $result = $q->execute(); - if ($result instanceof Cursor) { - $result = $result->toArray(); - foreach ($result as $targetObject) { - $slug = preg_replace("@^{$target}@smi", $replacement.$config['pathSeparator'], $targetObject[$config['slug']]); - $dm - ->createQueryBuilder() - ->update($config['useObjectClass']) - ->field($config['slug'])->set($slug) - ->field($meta->identifier)->equals($targetObject['_id']) - ->getQuery() - ->execute() - ; - } + $result = $q->getIterator(); + $count = 0; + + foreach ($result as $targetObject) { + ++$count; + $slug = preg_replace("@^{$target}@smi", $replacement.$config['pathSeparator'], $targetObject[$config['slug']]); + $dm + ->createQueryBuilder() + ->updateMany($config['useObjectClass']) + ->field($config['slug'])->set($slug) + ->field($meta->getIdentifier()[0])->equals($targetObject['_id']) + ->getQuery() + ->execute() + ; } + + return $count; } /** * This query can cause some data integrity failures since it does not * execute atomically * - * {@inheritDoc} + * {@inheritdoc} */ public function replaceInverseRelative($object, array $config, $target, $replacement) { @@ -100,24 +100,26 @@ public function replaceInverseRelative($object, array $config, $target, $replace $meta = $dm->getClassMetadata($config['useObjectClass']); $q = $dm ->createQueryBuilder($config['useObjectClass']) - ->field($config['mappedBy'].'.'.$meta->identifier)->equals($wrapped->getIdentifier()) + ->field($config['mappedBy'].'.'.$meta->getIdentifier()[0])->equals($wrapped->getIdentifier()) ->getQuery() ; $q->setHydrate(false); - $result = $q->execute(); - if ($result instanceof Cursor) { - $result = $result->toArray(); - foreach ($result as $targetObject) { - $slug = preg_replace("@^{$replacement}@smi", $target, $targetObject[$config['slug']]); - $dm - ->createQueryBuilder() - ->update($config['useObjectClass']) - ->field($config['slug'])->set($slug) - ->field($meta->identifier)->equals($targetObject['_id']) - ->getQuery() - ->execute() - ; - } + $result = $q->getIterator(); + $count = 0; + + foreach ($result as $targetObject) { + ++$count; + $slug = preg_replace("@^{$replacement}@smi", $target, $targetObject[$config['slug']]); + $dm + ->createQueryBuilder() + ->updateMany($config['useObjectClass']) + ->field($config['slug'])->set($slug) + ->field($meta->getIdentifier()[0])->equals($targetObject['_id']) + ->getQuery() + ->execute() + ; } + + return $count; } } diff --git a/lib/Gedmo/Sluggable/Mapping/Event/Adapter/ORM.php b/src/Sluggable/Mapping/Event/Adapter/ORM.php similarity index 63% rename from lib/Gedmo/Sluggable/Mapping/Event/Adapter/ORM.php rename to src/Sluggable/Mapping/Event/Adapter/ORM.php index f465ad637c..2b7a543845 100644 --- a/lib/Gedmo/Sluggable/Mapping/Event/Adapter/ORM.php +++ b/src/Sluggable/Mapping/Event/Adapter/ORM.php @@ -1,28 +1,38 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sluggable\Mapping\Event\Adapter; -use Doctrine\ORM\Mapping\ClassMetadataInfo; -use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; +use Doctrine\ORM\Mapping\ClassMetadata as EntityClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataInfo as LegacyEntityClassMetadata; use Doctrine\ORM\Query; +use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; use Gedmo\Tool\Wrapper\AbstractWrapper; +use Gedmo\Tool\Wrapper\EntityWrapper; +use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; +use Gedmo\Translatable\Translatable; /** * Doctrine event adapter for ORM adapted * for sluggable behavior * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ class ORM extends BaseAdapterORM implements SluggableAdapter { - /** - * {@inheritDoc} - */ public function getSimilarSlugs($object, $meta, array $config, $slug) { $em = $this->getObjectManager(); + /** @var EntityWrapper $wrapped */ $wrapped = AbstractWrapper::wrap($object, $em); $qb = $em->createQueryBuilder(); $qb->select('rec.'.$config['slug']) @@ -32,7 +42,7 @@ public function getSimilarSlugs($object, $meta, array $config, $slug) ':slug') ) ; - $qb->setParameter('slug',$slug.'%'); + $qb->setParameter('slug', $slug.'%'); // use the unique_base to restrict the uniqueness check if ($config['unique'] && isset($config['unique_base'])) { @@ -42,15 +52,17 @@ public function getSimilarSlugs($object, $meta, array $config, $slug) } else { $mapping = false; } - if ($ubase && !$mapping) { + if (($ubase || 0 === $ubase) && !$mapping) { $qb->andWhere('rec.'.$config['unique_base'].' = :unique_base'); $qb->setParameter(':unique_base', $ubase); - } elseif ($ubase && $mapping && in_array($mapping['type'], array(ClassMetadataInfo::ONE_TO_ONE, ClassMetadataInfo::MANY_TO_ONE))) { + } elseif ($ubase && $mapping && in_array($mapping['type'], [EntityClassMetadata::ONE_TO_ONE, EntityClassMetadata::MANY_TO_ONE], true)) { $mappedAlias = 'mapped_'.$config['unique_base']; $wrappedUbase = AbstractWrapper::wrap($ubase, $em); + $metadata = $wrappedUbase->getMetadata(); + assert($metadata instanceof EntityClassMetadata || $metadata instanceof LegacyEntityClassMetadata); $qb->innerJoin('rec.'.$config['unique_base'], $mappedAlias); foreach (array_keys($mapping['targetToSourceKeyColumns']) as $i => $mappedKey) { - $mappedProp = $wrappedUbase->getMetadata()->fieldNames[$mappedKey]; + $mappedProp = $metadata->getFieldName($mappedKey); $qb->andWhere($qb->expr()->eq($mappedAlias.'.'.$mappedProp, ':assoc'.$i)); $qb->setParameter(':assoc'.$i, $wrappedUbase->getPropertyValue($mappedProp)); } @@ -62,19 +74,26 @@ public function getSimilarSlugs($object, $meta, array $config, $slug) // include identifiers foreach ((array) $wrapped->getIdentifier(false) as $id => $value) { if (!$meta->isIdentifier($config['slug'])) { - $qb->andWhere($qb->expr()->neq('rec.'.$id, ':'.$id)); - $qb->setParameter($id, $value); + $namedId = str_replace('.', '_', $id); + $qb->andWhere($qb->expr()->neq('rec.'.$id, ':'.$namedId)); + $qb->setParameter($namedId, $value, $meta->getTypeOfField($namedId)); } } - $q = $qb->getQuery(); - $q->setHydrationMode(Query::HYDRATE_ARRAY); - return $q->execute(); + $query = $qb->getQuery(); + $query->setHydrationMode(Query::HYDRATE_ARRAY); + // Force translation walker to look for slug translations to avoid duplicated slugs + // TODO: Remove isset when removing support of YAML driver + if (isset($config['uniqueOverTranslations']) && $config['uniqueOverTranslations'] && $object instanceof Translatable) { + $query->setHint( + Query::HINT_CUSTOM_OUTPUT_WALKER, + TranslationWalker::class + ); + } + + return $query->getArrayResult(); } - /** - * {@inheritDoc} - */ public function replaceRelative($object, array $config, $target, $replacement) { $em = $this->getObjectManager(); @@ -82,7 +101,7 @@ public function replaceRelative($object, array $config, $target, $replacement) $qb->update($config['useObjectClass'], 'rec') ->set('rec.'.$config['slug'], $qb->expr()->concat( $qb->expr()->literal($replacement), - $qb->expr()->substring('rec.'.$config['slug'], strlen($target)) + $qb->expr()->substring('rec.'.$config['slug'], mb_strlen($target)) )) ->where($qb->expr()->like( 'rec.'.$config['slug'], @@ -95,9 +114,6 @@ public function replaceRelative($object, array $config, $target, $replacement) return $q->execute(); } - /** - * {@inheritDoc} - */ public function replaceInverseRelative($object, array $config, $target, $replacement) { $em = $this->getObjectManager(); @@ -105,9 +121,9 @@ public function replaceInverseRelative($object, array $config, $target, $replace $qb->update($config['useObjectClass'], 'rec') ->set('rec.'.$config['slug'], $qb->expr()->concat( $qb->expr()->literal($target), - $qb->expr()->substring('rec.'.$config['slug'], strlen($replacement)+1) + $qb->expr()->substring('rec.'.$config['slug'], mb_strlen($replacement) + 1) )) - ->where($qb->expr()->like('rec.'.$config['slug'], $qb->expr()->literal($replacement . '%'))) + ->where($qb->expr()->like('rec.'.$config['slug'], $qb->expr()->literal($replacement.'%'))) ; $q = $qb->getQuery(); diff --git a/src/Sluggable/Mapping/Event/SluggableAdapter.php b/src/Sluggable/Mapping/Event/SluggableAdapter.php new file mode 100644 index 0000000000..6380863326 --- /dev/null +++ b/src/Sluggable/Mapping/Event/SluggableAdapter.php @@ -0,0 +1,65 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sluggable\Mapping\Event; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Mapping\Event\AdapterInterface; +use Gedmo\Sluggable\SluggableListener; + +/** + * Doctrine event adapter for the Sluggable extension. + * + * @author Gediminas Morkevicius + * + * @phpstan-import-type SluggableConfiguration from SluggableListener + * @phpstan-import-type SlugConfiguration from SluggableListener + */ +interface SluggableAdapter extends AdapterInterface +{ + /** + * Loads the similar slugs for a managed object. + * + * @param object $object + * @param ClassMetadata $meta + * @param string $slug + * + * @phpstan-param SlugConfiguration $config + * + * @return array> + */ + public function getSimilarSlugs($object, $meta, array $config, $slug); + + /** + * Replace part of a slug on all objects matching the target pattern. + * + * @param object $object + * @param string $target + * @param string $replacement + * + * @phpstan-param SlugConfiguration $config + * + * @return int the number of updated records + */ + public function replaceRelative($object, array $config, $target, $replacement); + + /** + * Replace part of a slug on all objects matching the target pattern + * and having a relation to the managed object. + * + * @param object $object + * @param string $target + * @param string $replacement + * + * @phpstan-param SluggableConfiguration $config + * + * @return int the number of updated records + */ + public function replaceInverseRelative($object, array $config, $target, $replacement); +} diff --git a/lib/Gedmo/Sluggable/Sluggable.php b/src/Sluggable/Sluggable.php similarity index 58% rename from lib/Gedmo/Sluggable/Sluggable.php rename to src/Sluggable/Sluggable.php index db64e13fc2..04fa18a3d9 100644 --- a/lib/Gedmo/Sluggable/Sluggable.php +++ b/src/Sluggable/Sluggable.php @@ -1,40 +1,45 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sluggable; /** - * This interface is not necessary but can be implemented for - * Entities which in some cases needs to be identified as - * Sluggable + * Marker interface for objects which can be identified as sluggable. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ interface Sluggable { // use now annotations instead of predefined methods, this interface is not necessary - /** - * @gedmo:Sluggable - * to mark the field as sluggable use property annotation @gedmo:Sluggable + /* + * @Gedmo\Sluggable + * to mark the field as sluggable use property annotation @Gedmo\Sluggable * this field value will be included in built slug */ - /** - * @gedmo:Slug - to mark property which will hold slug use annotation @gedmo:Slug + /* + * @Gedmo\Slug - to mark property which will hold slug use annotation @Gedmo\Slug * available options: * updatable (optional, default=true) - true to update the slug on sluggable field changes, false - otherwise * unique (optional, default=true) - true if slug should be unique and if identical it will be prefixed, false - otherwise * unique_base (optional, default="") - used in conjunction with unique. The name of the entity property that should be used as a key when doing a uniqueness check * separator (optional, default="-") - separator which will separate words in slug - * prefix (optional, default="") - suffix which will be added to the generated slug - * suffix (optional, default="") - prefix which will be added to the generated slug + * prefix (optional, default="") - prefix which will be added to the generated slug + * suffix (optional, default="") - suffix which will be added to the generated slug * style (optional, default="default") - "default" all letters will be lowercase, "camel" - first word letter will be uppercase * dateFormat (optional, default="default") - "default" all letters will be lowercase, "camel" - first word letter will be uppercase + * uniqueOverTranslations (optional, default=false) - true if slug should be unique over translations and if identical it will be prefixed, false - otherwise * * example: * - * @gedmo:Slug(style="camel", separator="_", prefix="", suffix="", updatable=false, unique=false) + * @Gedmo\Slug(style="camel", separator="_", prefix="", suffix="", updatable=false, unique=false) * @Column(type="string", length=64) * $property */ diff --git a/lib/Gedmo/Sluggable/SluggableListener.php b/src/Sluggable/SluggableListener.php similarity index 60% rename from lib/Gedmo/Sluggable/SluggableListener.php rename to src/Sluggable/SluggableListener.php index 44c472ada7..9ddd8401be 100644 --- a/lib/Gedmo/Sluggable/SluggableListener.php +++ b/src/Sluggable/SluggableListener.php @@ -1,12 +1,28 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sluggable; use Doctrine\Common\EventArgs; +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Event\ManagerEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\InvalidArgumentException; use Gedmo\Mapping\MappedEventSubscriber; +use Gedmo\Sluggable\Handler\SlugHandlerInterface; +use Gedmo\Sluggable\Handler\SlugHandlerWithUniqueCallbackInterface; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; -use Doctrine\Common\Persistence\ObjectManager; -use Gedmo\Tool\Wrapper\AbstractWrapper; +use Symfony\Component\String\Slugger\AsciiSlugger; + +use function Symfony\Component\String\u; /** * The SluggableListener handles the generation of slugs @@ -17,31 +33,64 @@ * * @author Gediminas Morkevicius * @author Klein Florian - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @phpstan-type SluggableConfiguration = array{ + * mappedBy?: string, + * pathSeparator?: string, + * slug?: string, + * slugs?: array, + * unique?: bool, + * useObjectClass?: class-string, + * } + * @phpstan-type SlugConfiguration = array{ + * fields: string[], + * slug: string, + * style: string, + * dateFormat: string, + * pathSeparator?: string, + * updatable: bool, + * unique: bool, + * unique_base: string, + * separator: string, + * prefix: string, + * suffix: string, + * handlers: array, + * uniqueOverTranslations: bool, + * useObjectClass?: class-string, + * } + * + * @phpstan-extends MappedEventSubscriber */ class SluggableListener extends MappedEventSubscriber { /** * The power exponent to jump * the slug unique number by tens. - * - * @var integer */ - private $exponent = 0; + private int $exponent = 0; /** * Transliteration callback for slugs * - * @var callable + * @var callable(string, string, object): string */ - private $transliterator = array('Gedmo\Sluggable\Util\Urlizer', 'transliterate'); + private $transliterator; /** * Urlize callback for slugs * - * @var callable + * @var callable(string, string, object): string */ - private $urlizer = array('Gedmo\Sluggable\Util\Urlizer', 'urlize'); + private $urlizer; /** * List of inserted slugs for each object class. @@ -49,36 +98,62 @@ class SluggableListener extends MappedEventSubscriber * composition in number of persisted objects * during the same flush * - * @var array + * @var array> + * + * @phpstan-var array> */ - private $persisted = array(); + private array $persisted = []; /** * List of initialized slug handlers * - * @var array + * @var array + * + * @phpstan-var array, SlugHandlerInterface> */ - private $handlers = array(); + private array $handlers = []; /** * List of filters which are manipulated when slugs are generated * - * @var array + * @var array> */ - private $managedFilters = array(); + private array $managedFilters = []; + + public function __construct() + { + parent::__construct(); + + $this->setTransliterator( + static fn (string $text, string $separator, object $object): string => u($text)->ascii()->toString() + ); + + /* + * Note - Requiring the call to `lower()` in this chain contradicts with the `style` configuration + * which doesn't require or enforce lowercase styling by default, but the Behat transliterator applied + * this styling so it is used for B/C + */ + + $this->setUrlizer( + static fn (string $text, string $separator, object $object): string => (new AsciiSlugger()) + ->slug($text, $separator) + ->lower() + ->toString() + ); + } /** * Specifies the list of events to listen * - * @return array + * @return string[] */ public function getSubscribedEvents() { - return array( + return [ 'onFlush', 'loadClassMetadata', 'prePersist', - ); + ]; } /** @@ -87,14 +162,16 @@ public function getSubscribedEvents() * * @param callable $callable * - * @throws \Gedmo\Exception\InvalidArgumentException + * @phpstan-param callable(string $text, string $separator, object $object): string $callable + * + * @throws InvalidArgumentException * * @return void */ public function setTransliterator($callable) { if (!is_callable($callable)) { - throw new \Gedmo\Exception\InvalidArgumentException('Invalid transliterator callable parameter given'); + throw new InvalidArgumentException('Invalid transliterator callable parameter given'); } $this->transliterator = $callable; } @@ -104,11 +181,17 @@ public function setTransliterator($callable) * to urlize slugs * * @param callable $callable + * + * @phpstan-param callable(string $text, string $separator, object $object): string $callable + * + * @throws InvalidArgumentException + * + * @return void */ public function setUrlizer($callable) { if (!is_callable($callable)) { - throw new \Gedmo\Exception\InvalidArgumentException('Invalid urlizer callable parameter given'); + throw new InvalidArgumentException('Invalid urlizer callable parameter given'); } $this->urlizer = $callable; } @@ -117,6 +200,8 @@ public function setUrlizer($callable) * Get currently used transliterator callable * * @return callable + * + * @phpstan-return callable(string $text, string $separator, object $object): string */ public function getTransliterator() { @@ -127,6 +212,8 @@ public function getTransliterator() * Get currently used urlizer callable * * @return callable + * + * @phpstan-return callable(string $text, string $separator, object $object): string */ public function getUrlizer() { @@ -138,16 +225,20 @@ public function getUrlizer() * * @param string $name * @param bool $disable True by default + * + * @return void */ public function addManagedFilter($name, $disable = true) { - $this->managedFilters[$name] = array('disabled' => $disable); + $this->managedFilters[$name] = ['disabled' => $disable]; } /** * Removes a filter from the managed set * * @param string $name + * + * @return void */ public function removeManagedFilter($name) { @@ -157,20 +248,23 @@ public function removeManagedFilter($name) /** * Mapps additional metadata * - * @param EventArgs $eventArgs + * @param LoadClassMetadataEventArgs $eventArgs + * + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { - $ea = $this->getEventAdapter($eventArgs); - $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata()); + $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } /** * Allows identifier fields to be slugged as usual * - * @param EventArgs $args + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args * * @return void */ @@ -181,10 +275,10 @@ public function prePersist(EventArgs $args) $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - if ($config = $this->getConfiguration($om, $meta->name)) { + if ($config = $this->getConfiguration($om, $meta->getName())) { foreach ($config['slugs'] as $slugField => $options) { if ($meta->isIdentifier($slugField)) { - $meta->getReflectionProperty($slugField)->setValue($object, '__id__'); + $meta->setFieldValue($object, $slugField, uniqid('__sluggable_placeholder__')); } } } @@ -194,13 +288,15 @@ public function prePersist(EventArgs $args) * Generate slug on objects being updated during flush * if they require changing * - * @param EventArgs $args + * @param ManagerEventArgs $args + * + * @phpstan-param ManagerEventArgs $args * * @return void */ public function onFlush(EventArgs $args) { - $this->persisted = array(); + $this->persisted = []; $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); @@ -212,7 +308,7 @@ public function onFlush(EventArgs $args) // ensure correct result. No additional overhead is encountered foreach ($ea->getScheduledObjectInsertions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name)) { + if ($this->getConfiguration($om, $meta->getName())) { // generate first to exclude this object from similar persisted slugs result $this->generateSlug($ea, $object); $this->persisted[$ea->getRootObjectClass($meta)][] = $object; @@ -222,20 +318,15 @@ public function onFlush(EventArgs $args) // event listeners be nested together foreach ($ea->getScheduledObjectUpdates($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name) && !$uow->isScheduledForInsert($object)) { + if ($this->getConfiguration($om, $meta->getName()) && !$uow->isScheduledForInsert($object)) { $this->generateSlug($ea, $object); $this->persisted[$ea->getRootObjectClass($meta)][] = $object; } } $this->manageFiltersAfterGeneration($om); - - AbstractWrapper::clear(); } - /** - * {@inheritDoc} - */ protected function getNamespace() { return __NAMESPACE__; @@ -244,11 +335,9 @@ protected function getNamespace() /** * Get the slug handler instance by $class name * - * @param string $class - * - * @return \Gedmo\Sluggable\Handler\SlugHandlerInterface + * @phpstan-param class-string $class */ - private function getHandler($class) + private function getHandler(string $class): SlugHandlerInterface { if (!isset($this->handlers[$class])) { $this->handlers[$class] = new $class($this); @@ -259,29 +348,24 @@ private function getHandler($class) /** * Creates the slug for object being flushed - * - * @param SluggableAdapter $ea - * @param object $object - * - * @return void */ - private function generateSlug(SluggableAdapter $ea, $object) + private function generateSlug(SluggableAdapter $ea, object $object): void { $om = $ea->getObjectManager(); $meta = $om->getClassMetadata(get_class($object)); $uow = $om->getUnitOfWork(); $changeSet = $ea->getObjectChangeSet($uow, $object); $isInsert = $uow->isScheduledForInsert($object); - $config = $this->getConfiguration($om, $meta->name); + $config = $this->getConfiguration($om, $meta->getName()); foreach ($config['slugs'] as $slugField => $options) { - $hasHandlers = count($options['handlers']); + $hasHandlers = [] !== $options['handlers']; $options['useObjectClass'] = $config['useObjectClass']; // collect the slug from fields - $slug = $meta->getReflectionProperty($slugField)->getValue($object); + $slug = $meta->getFieldValue($object, $slugField); // if slug should not be updated, skip it - if (!$options['updatable'] && !$isInsert && (!isset($changeSet[$slugField]) || $slug === '__id__')) { + if (!$options['updatable'] && !$isInsert && (!isset($changeSet[$slugField]) || 0 === strpos($slug, '__sluggable_placeholder__'))) { continue; } // must fetch the old slug from changeset, since $object holds the new version @@ -289,15 +373,15 @@ private function generateSlug(SluggableAdapter $ea, $object) $needToChangeSlug = false; // if slug is null, regenerate it, or needs an update - if (null === $slug || $slug === '__id__' || !isset($changeSet[$slugField])) { + if (null === $slug || 0 === strpos($slug, '__sluggable_placeholder__') || !isset($changeSet[$slugField])) { $slug = ''; foreach ($options['fields'] as $sluggableField) { if (isset($changeSet[$sluggableField]) || isset($changeSet[$slugField])) { $needToChangeSlug = true; } - $value = $meta->getReflectionProperty($sluggableField)->getValue($object); - $slug .= ($value instanceof \DateTime) ? $value->format($options['dateFormat']) : $value; + $value = $meta->getFieldValue($object, $sluggableField); + $slug .= $value instanceof \DateTimeInterface ? $value->format($options['dateFormat']) : $value; $slug .= ' '; } // trim generated slug as it will have unnecessary trailing space @@ -331,14 +415,14 @@ private function generateSlug(SluggableAdapter $ea, $object) // Step 1: transliteration, changing ๅŒ—ไบฌ to 'Bei Jing' $slug = call_user_func_array( $this->transliterator, - array($slug, $options['separator'], $object) + [$slug, $options['separator'], $object] ); // Step 2: urlization (replace spaces by '-' etc...) if (!$urlized) { $slug = call_user_func_array( $this->urlizer, - array($slug, $options['separator'], $object) + [$slug, $options['separator'], $object] ); } @@ -349,25 +433,22 @@ private function generateSlug(SluggableAdapter $ea, $object) switch ($options['style']) { case 'camel': $quotedSeparator = preg_quote($options['separator']); - $slug = preg_replace_callback('/^[a-z]|'.$quotedSeparator.'[a-z]/smi', function ($m) { - return strtoupper($m[0]); - }, $slug); + $slug = preg_replace_callback( + '/^[a-z]|'.$quotedSeparator.'[a-z]/smi', + static fn (array $m): string => u($m[0])->upper()->toString(), + $slug + ); + break; case 'lower': - if (function_exists('mb_strtolower')) { - $slug = mb_strtolower($slug); - } else { - $slug = strtolower($slug); - } + $slug = u($slug)->lower()->toString(); + break; case 'upper': - if (function_exists('mb_strtoupper')) { - $slug = mb_strtoupper($slug); - } else { - $slug = strtoupper($slug); - } + $slug = u($slug)->upper()->toString(); + break; default: @@ -376,14 +457,25 @@ private function generateSlug(SluggableAdapter $ea, $object) } // cut slug if exceeded in length - if (isset($mapping['length']) && strlen($slug) > $mapping['length']) { - $slug = substr($slug, 0, $mapping['length']); + $length = $mapping->length ?? $mapping['length'] ?? null; + if (null !== $length && strlen($slug) > $length) { + $slug = substr($slug, 0, $length); } - if (isset($mapping['nullable']) && $mapping['nullable'] && !$slug) { + if (($mapping->nullable ?? $mapping['nullable'] ?? false) && 0 === strlen($slug)) { $slug = null; } + // notify slug handlers --> beforeMakingUnique + if ($hasHandlers) { + foreach ($options['handlers'] as $class => $handlerOptions) { + $handler = $this->getHandler($class); + if ($handler instanceof SlugHandlerWithUniqueCallbackInterface) { + $handler->beforeMakingUnique($ea, $options, $object, $slug); + } + } + } + // make unique slug if requested if ($options['unique'] && null !== $slug) { $this->exponent = 0; @@ -398,12 +490,11 @@ private function generateSlug(SluggableAdapter $ea, $object) } // set the final slug - $meta->getReflectionProperty($slugField)->setValue($object, $slug); + $meta->setFieldValue($object, $slugField, $slug); // recompute changeset $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); // overwrite changeset (to set old value) $uow->propertyChanged($object, $slugField, $oldSlug, $slug); - } } } @@ -411,42 +502,37 @@ private function generateSlug(SluggableAdapter $ea, $object) /** * Generates the unique slug * - * @param SluggableAdapter $ea - * @param object $object - * @param string $preferredSlug - * @param boolean $recursing - * @param array $config[$slugField] - * - * @return string - unique slug + * @param SlugConfiguration $config */ - private function makeUniqueSlug(SluggableAdapter $ea, $object, $preferredSlug, $recursing = false, $config = array()) + private function makeUniqueSlug(SluggableAdapter $ea, object $object, string $preferredSlug, bool $recursing, array $config): string { $om = $ea->getObjectManager(); $meta = $om->getClassMetadata(get_class($object)); - $similarPersisted = array(); + $similarPersisted = []; // extract unique base $base = false; if ($config['unique'] && isset($config['unique_base'])) { - $base = $meta->getReflectionProperty($config['unique_base'])->getValue($object); + $base = $meta->getFieldValue($object, $config['unique_base']); } // collect similar persisted slugs during this flush if (isset($this->persisted[$class = $ea->getRootObjectClass($meta)])) { foreach ($this->persisted[$class] as $obj) { - if ($base !== false && $meta->getReflectionProperty($config['unique_base'])->getValue($obj) !== $base) { + if (false !== $base && $meta->getFieldValue($obj, $config['unique_base']) !== $base) { continue; // if unique_base field is not the same, do not take slug as similar } - $slug = $meta->getReflectionProperty($config['slug'])->getValue($obj); + $slug = $meta->getFieldValue($obj, $config['slug']); $quotedPreferredSlug = preg_quote($preferredSlug); if (preg_match("@^{$quotedPreferredSlug}.*@smi", $slug)) { - $similarPersisted[] = array($config['slug'] => $slug); + $similarPersisted[] = [$config['slug'] => $slug]; } } } // load similar slugs - $result = array_merge((array) $ea->getSimilarSlugs($object, $meta, $config, $preferredSlug), $similarPersisted); + $result = [...$ea->getSimilarSlugs($object, $meta, $config, $preferredSlug), ...$similarPersisted]; + // leave only right slugs if (!$recursing) { @@ -462,29 +548,32 @@ private function makeUniqueSlug(SluggableAdapter $ea, $object, $preferredSlug, $ if ($result) { $generatedSlug = $preferredSlug; - $sameSlugs = array(); + $sameSlugs = []; foreach ((array) $result as $list) { $sameSlugs[] = $list[$config['slug']]; } - $i = pow(10, $this->exponent); - if ($recursing || in_array($generatedSlug, $sameSlugs)) { + $i = 10 ** $this->exponent; + $uniqueSuffix = (string) $i; + if ($recursing || in_array($generatedSlug, $sameSlugs, true)) { do { - $generatedSlug = $preferredSlug.$config['separator'].$i++; - } while (in_array($generatedSlug, $sameSlugs)); + $generatedSlug = $preferredSlug.$config['separator'].$uniqueSuffix; + $uniqueSuffix = (string) ++$i; + } while (in_array($generatedSlug, $sameSlugs, true)); } $mapping = $meta->getFieldMapping($config['slug']); - if (isset($mapping['length']) && strlen($generatedSlug) > $mapping['length']) { + $length = $mapping->length ?? $mapping['length'] ?? null; + if (null !== $length && strlen($generatedSlug) > $length) { $generatedSlug = substr( $generatedSlug, 0, - $mapping['length'] - (strlen($i) + strlen($config['separator'])) + $length - (strlen($uniqueSuffix) + strlen($config['separator'])) ); - $this->exponent = strlen($i) - 1; - if (substr($generatedSlug,-strlen($config['separator'])) == $config['separator']) { - $generatedSlug = substr($generatedSlug,0,strlen($generatedSlug) - strlen($config['separator'])); + $this->exponent = strlen($uniqueSuffix) - 1; + if (substr($generatedSlug, -strlen($config['separator'])) == $config['separator']) { + $generatedSlug = substr($generatedSlug, 0, strlen($generatedSlug) - strlen($config['separator'])); } $generatedSlug = $this->makeUniqueSlug($ea, $object, $generatedSlug, true, $config); } @@ -494,10 +583,7 @@ private function makeUniqueSlug(SluggableAdapter $ea, $object, $preferredSlug, $ return $preferredSlug; } - /** - * @param \Doctrine\Common\Persistence\ObjectManager $om - */ - private function manageFiltersBeforeGeneration(ObjectManager $om) + private function manageFiltersBeforeGeneration(ObjectManager $om): void { $collection = $this->getFilterCollectionFromObjectManager($om); @@ -505,7 +591,7 @@ private function manageFiltersBeforeGeneration(ObjectManager $om) // set each managed filter to desired status foreach ($this->managedFilters as $name => &$config) { - $enabled = in_array($name, $enabledFilters); + $enabled = in_array($name, $enabledFilters, true); $config['previouslyEnabled'] = $enabled; if ($config['disabled']) { @@ -518,16 +604,13 @@ private function manageFiltersBeforeGeneration(ObjectManager $om) } } - /** - * @param \Doctrine\Common\Persistence\ObjectManager $om - */ - private function manageFiltersAfterGeneration(ObjectManager $om) + private function manageFiltersAfterGeneration(ObjectManager $om): void { $collection = $this->getFilterCollectionFromObjectManager($om); // Restore managed filters to their original status foreach ($this->managedFilters as $name => &$config) { - if ($config['previouslyEnabled'] === true) { + if (true === $config['previouslyEnabled']) { $collection->enable($name); } @@ -538,20 +621,19 @@ private function manageFiltersAfterGeneration(ObjectManager $om) /** * Retrieves a FilterCollection instance from the given ObjectManager. * - * @param \Doctrine\Common\Persistence\ObjectManager $om - * - * @throws \Gedmo\Exception\InvalidArgumentException + * @throws InvalidArgumentException * * @return mixed */ private function getFilterCollectionFromObjectManager(ObjectManager $om) { - if (is_callable(array($om, 'getFilters'))) { + if (is_callable([$om, 'getFilters'])) { return $om->getFilters(); - } elseif (is_callable(array($om, 'getFilterCollection'))) { + } + if (is_callable([$om, 'getFilterCollection'])) { return $om->getFilterCollection(); } - throw new \Gedmo\Exception\InvalidArgumentException("ObjectManager does not support filters"); + throw new InvalidArgumentException('ObjectManager does not support filters'); } } diff --git a/src/Sluggable/Util/Urlizer.php b/src/Sluggable/Util/Urlizer.php new file mode 100644 index 0000000000..ba3d0710a6 --- /dev/null +++ b/src/Sluggable/Util/Urlizer.php @@ -0,0 +1,28 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sluggable\Util; + +use Behat\Transliterator\Transliterator; +use Gedmo\Exception\RuntimeException; + +if (!class_exists(Transliterator::class)) { + throw new RuntimeException(sprintf('Cannot use the "%s" class when the "behat/transliterator" package is not installed.', Urlizer::class)); +} + +/** + * Transliteration utility + * + * @deprecated since gedmo/doctrine-extensions 3.21, will be removed in version 4.0. + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class Urlizer extends Transliterator +{ +} diff --git a/src/SoftDeleteable/Event/PostSoftDeleteEventArgs.php b/src/SoftDeleteable/Event/PostSoftDeleteEventArgs.php new file mode 100644 index 0000000000..f8a41338b2 --- /dev/null +++ b/src/SoftDeleteable/Event/PostSoftDeleteEventArgs.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Event; + +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\ObjectManager; + +/** + * @template TObjectManager of ObjectManager + * + * @template-extends LifecycleEventArgs + */ +final class PostSoftDeleteEventArgs extends LifecycleEventArgs +{ +} diff --git a/src/SoftDeleteable/Event/PreSoftDeleteEventArgs.php b/src/SoftDeleteable/Event/PreSoftDeleteEventArgs.php new file mode 100644 index 0000000000..e01cd73d86 --- /dev/null +++ b/src/SoftDeleteable/Event/PreSoftDeleteEventArgs.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Event; + +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\ObjectManager; + +/** + * @template TObjectManager of ObjectManager + * + * @template-extends LifecycleEventArgs + */ +final class PreSoftDeleteEventArgs extends LifecycleEventArgs +{ +} diff --git a/src/SoftDeleteable/Filter/ODM/SoftDeleteableFilter.php b/src/SoftDeleteable/Filter/ODM/SoftDeleteableFilter.php new file mode 100644 index 0000000000..12d80c0b4b --- /dev/null +++ b/src/SoftDeleteable/Filter/ODM/SoftDeleteableFilter.php @@ -0,0 +1,139 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Filter\ODM; + +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\Query\Filter\BsonFilter; +use Gedmo\SoftDeleteable\SoftDeleteableListener; + +/** + * @final since gedmo/doctrine-extensions 3.11 + */ +class SoftDeleteableFilter extends BsonFilter +{ + /** + * @var SoftDeleteableListener|null + */ + protected $listener; + + /** + * @var DocumentManager|null + * + * @deprecated `BsonFilter::$dm` is a protected property, thus this property is not required + */ + protected $documentManager; + + /** + * @var array + */ + protected $disabled = []; + + /** + * Gets the criteria part to add to a query. + * + * @return array|null>>|null> The criteria array, if there is available, empty array otherwise + * + * @phpstan-return array>|null> + */ + public function addFilterCriteria(ClassMetadata $targetEntity): array + { + $class = $targetEntity->getName(); + if (true === ($this->disabled[$class] ?? false)) { + return []; + } + if (true === ($this->disabled[$targetEntity->rootDocumentName] ?? false)) { + return []; + } + + $config = $this->getListener()->getConfiguration($this->getDocumentManager(), $targetEntity->name); + + if (!isset($config['softDeleteable']) || !$config['softDeleteable']) { + return []; + } + + $column = $targetEntity->getFieldMapping($config['fieldName']); + + if (isset($config['timeAware']) && $config['timeAware']) { + return [ + '$or' => [ + [$column['fieldName'] => null], + [$column['fieldName'] => ['$gt' => new \DateTime()]], + ], + ]; + } + + return [ + $column['fieldName'] => null, + ]; + } + + /** + * @param string $class + * + * @phpstan-param class-string $class + * + * @return void + */ + public function disableForDocument($class) + { + $this->disabled[$class] = true; + } + + /** + * @param string $class + * + * @phpstan-param class-string $class + * + * @return void + */ + public function enableForDocument($class) + { + $this->disabled[$class] = false; + } + + /** + * @return SoftDeleteableListener + */ + protected function getListener() + { + if (null === $this->listener) { + $em = $this->getDocumentManager(); + $evm = $em->getEventManager(); + + foreach ($evm->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { + if ($listener instanceof SoftDeleteableListener) { + $this->listener = $listener; + + break 2; + } + } + } + + if (null === $this->listener) { + throw new \RuntimeException('Listener "SoftDeleteableListener" was not added to the EventManager!'); + } + } + + return $this->listener; + } + + /** + * @return DocumentManager + */ + protected function getDocumentManager() + { + // Remove the following assignment on the next major release. + $this->documentManager = $this->dm; + + return $this->dm; + } +} diff --git a/src/SoftDeleteable/Filter/SoftDeleteableFilter.php b/src/SoftDeleteable/Filter/SoftDeleteableFilter.php new file mode 100644 index 0000000000..12ed75f48d --- /dev/null +++ b/src/SoftDeleteable/Filter/SoftDeleteableFilter.php @@ -0,0 +1,151 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Filter; + +use Doctrine\DBAL\Exception; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query\Filter\SQLFilter; +use Gedmo\SoftDeleteable\SoftDeleteableListener; + +/** + * The SoftDeleteableFilter adds the condition necessary to + * filter entities which were deleted "softly" + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * @author Patrik Votoฤek + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class SoftDeleteableFilter extends SQLFilter +{ + /** + * @var SoftDeleteableListener + */ + protected $listener; + + /** + * @var EntityManagerInterface + */ + protected $entityManager; + + /** + * @var array + * + * @phpstan-var array + */ + protected $disabled = []; + + /** + * @param string $targetTableAlias + * + * @throws Exception + */ + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string + { + $class = $targetEntity->getName(); + if (true === ($this->disabled[$class] ?? false)) { + return ''; + } + if (true === ($this->disabled[$targetEntity->rootEntityName] ?? false)) { + return ''; + } + + $config = $this->getListener()->getConfiguration($this->getEntityManager(), $targetEntity->name); + + if (!isset($config['softDeleteable']) || !$config['softDeleteable']) { + return ''; + } + + $platform = $this->getConnection()->getDatabasePlatform(); + $quoteStrategy = $this->getEntityManager()->getConfiguration()->getQuoteStrategy(); + + $column = $quoteStrategy->getColumnName($config['fieldName'], $targetEntity, $platform); + + $addCondSql = $targetTableAlias.'.'.$column.' IS NULL'; + if (isset($config['timeAware']) && $config['timeAware']) { + $addCondSql = "({$addCondSql} OR {$targetTableAlias}.{$column} > {$platform->getCurrentTimestampSQL()})"; + } + + return $addCondSql; + } + + /** + * @param string $class + * + * @phpstan-param class-string $class + * + * @return void + */ + public function disableForEntity($class) + { + $this->disabled[$class] = true; + // Make sure the hash (@see SQLFilter::__toString()) for this filter will be changed to invalidate the query cache. + $this->setParameter(sprintf('disabled_%s', $class), true); + } + + /** + * @param string $class + * + * @phpstan-param class-string $class + * + * @return void + */ + public function enableForEntity($class) + { + $this->disabled[$class] = false; + // Make sure the hash (@see SQLFilter::__toString()) for this filter will be changed to invalidate the query cache. + $this->setParameter(sprintf('disabled_%s', $class), false); + } + + /** + * @throws \RuntimeException + * + * @return SoftDeleteableListener + */ + protected function getListener() + { + if (null === $this->listener) { + $em = $this->getEntityManager(); + $evm = $em->getEventManager(); + + foreach ($evm->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { + if ($listener instanceof SoftDeleteableListener) { + $this->listener = $listener; + + break 2; + } + } + } + + if (null === $this->listener) { + throw new \RuntimeException('Listener "SoftDeleteableListener" was not added to the EventManager!'); + } + } + + return $this->listener; + } + + /** + * @return EntityManagerInterface + */ + protected function getEntityManager() + { + if (null === $this->entityManager) { + $getEntityManager = \Closure::bind(fn (): EntityManagerInterface => $this->em, $this, parent::class); + + $this->entityManager = $getEntityManager(); + } + + return $this->entityManager; + } +} diff --git a/src/SoftDeleteable/Mapping/Driver/Annotation.php b/src/SoftDeleteable/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..a45df5a33c --- /dev/null +++ b/src/SoftDeleteable/Mapping/Driver/Annotation.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the soft-deletable extension which reads extended metadata from annotations on a soft-deletable class. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/src/SoftDeleteable/Mapping/Driver/Attribute.php b/src/SoftDeleteable/Mapping/Driver/Attribute.php new file mode 100644 index 0000000000..8358f1b35b --- /dev/null +++ b/src/SoftDeleteable/Mapping/Driver/Attribute.php @@ -0,0 +1,71 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Mapping\Driver; + +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\SoftDeleteable; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\SoftDeleteable\Mapping\Validator; + +/** + * Mapping driver for the soft-deletable extension which reads extended metadata from attributes on a soft-deletable class. + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @internal + */ +class Attribute extends AbstractAnnotationDriver +{ + /** + * Mapping object for the soft-deletable extension. + */ + public const SOFT_DELETEABLE = SoftDeleteable::class; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // class annotations + if (null !== $class && $annot = $this->reader->getClassAnnotation($class, self::SOFT_DELETEABLE)) { + \assert($annot instanceof SoftDeleteable); + + $config['softDeleteable'] = true; + + Validator::validateField($meta, $annot->fieldName); + + $config['fieldName'] = $annot->fieldName; + + $config['timeAware'] = false; + + if (isset($annot->timeAware)) { + if (!is_bool($annot->timeAware)) { + throw new InvalidMappingException('timeAware must be boolean. '.gettype($annot->timeAware).' provided.'); + } + + $config['timeAware'] = $annot->timeAware; + } + + $config['hardDelete'] = true; + + if (isset($annot->hardDelete)) { + if (!is_bool($annot->hardDelete)) { + throw new InvalidMappingException('hardDelete must be boolean. '.gettype($annot->hardDelete).' provided.'); + } + + $config['hardDelete'] = $annot->hardDelete; + } + } + + $this->validateFullMetadata($meta, $config); + + return $config; + } +} diff --git a/lib/Gedmo/SoftDeleteable/Mapping/Driver/Xml.php b/src/SoftDeleteable/Mapping/Driver/Xml.php similarity index 65% rename from lib/Gedmo/SoftDeleteable/Mapping/Driver/Xml.php rename to src/SoftDeleteable/Mapping/Driver/Xml.php index 23241933ac..451733fd4c 100644 --- a/lib/Gedmo/SoftDeleteable/Mapping/Driver/Xml.php +++ b/src/SoftDeleteable/Mapping/Driver/Xml.php @@ -1,9 +1,16 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\SoftDeleteable\Mapping\Driver; -use Gedmo\Mapping\Driver\Xml as BaseXml; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver\Xml as BaseXml; use Gedmo\SoftDeleteable\Mapping\Validator; /** @@ -15,23 +22,21 @@ * @author Gustavo Falco * @author Gediminas Morkevicius * @author Miha Vrhovnik - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ class Xml extends BaseXml { - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { /** - * @var \SimpleXmlElement $xml + * @var \SimpleXmlElement */ - $xml = $this->_getMapping($meta->name); + $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); - if (in_array($xmlDoctrine->getName(), array('mapped-superclass', 'entity', 'document', 'embedded-document'))) { + if (in_array($xmlDoctrine->getName(), ['mapped-superclass', 'entity', 'document', 'embedded-document'], true)) { if (isset($xml->{'soft-deleteable'})) { $field = $this->_getAttribute($xml->{'soft-deleteable'}, 'field-name'); @@ -48,7 +53,14 @@ public function readExtendedMetadata($meta, array &$config) if ($this->_isAttributeSet($xml->{'soft-deleteable'}, 'time-aware')) { $config['timeAware'] = $this->_getBooleanAttribute($xml->{'soft-deleteable'}, 'time-aware'); } + + $config['hardDelete'] = true; + if ($this->_isAttributeSet($xml->{'soft-deleteable'}, 'hard-delete')) { + $config['hardDelete'] = $this->_getBooleanAttribute($xml->{'soft-deleteable'}, 'hard-delete'); + } } } + + return $config; } } diff --git a/lib/Gedmo/SoftDeleteable/Mapping/Driver/Yaml.php b/src/SoftDeleteable/Mapping/Driver/Yaml.php similarity index 61% rename from lib/Gedmo/SoftDeleteable/Mapping/Driver/Yaml.php rename to src/SoftDeleteable/Mapping/Driver/Yaml.php index c8249f8369..e2790b1b66 100644 --- a/lib/Gedmo/SoftDeleteable/Mapping/Driver/Yaml.php +++ b/src/SoftDeleteable/Mapping/Driver/Yaml.php @@ -1,10 +1,17 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\SoftDeleteable\Mapping\Driver; -use Gedmo\Mapping\Driver\File; -use Gedmo\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; use Gedmo\SoftDeleteable\Mapping\Validator; /** @@ -15,22 +22,23 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ class Yaml extends File implements Driver { /** * File extension + * * @var string */ protected $_extension = '.dcm.yml'; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['gedmo'])) { $classMapping = $mapping['gedmo']; @@ -50,17 +58,24 @@ public function readExtendedMetadata($meta, array &$config) $config['timeAware'] = false; if (isset($classMapping['soft_deleteable']['time_aware'])) { if (!is_bool($classMapping['soft_deleteable']['time_aware'])) { - throw new InvalidMappingException("timeAware must be boolean. ".gettype($classMapping['soft_deleteable']['time_aware'])." provided."); + throw new InvalidMappingException('timeAware must be boolean. '.gettype($classMapping['soft_deleteable']['time_aware']).' provided.'); } $config['timeAware'] = $classMapping['soft_deleteable']['time_aware']; } + + $config['hardDelete'] = true; + if (isset($classMapping['soft_deleteable']['hard_delete'])) { + if (!is_bool($classMapping['soft_deleteable']['hard_delete'])) { + throw new InvalidMappingException('hardDelete must be boolean. '.gettype($classMapping['soft_deleteable']['hard_delete']).' provided.'); + } + $config['hardDelete'] = $classMapping['soft_deleteable']['hard_delete']; + } } } + + return $config; } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); diff --git a/src/SoftDeleteable/Mapping/Event/Adapter/ODM.php b/src/SoftDeleteable/Mapping/Event/Adapter/ODM.php new file mode 100644 index 0000000000..c698d205c4 --- /dev/null +++ b/src/SoftDeleteable/Mapping/Event/Adapter/ODM.php @@ -0,0 +1,52 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Mapping\Event\Adapter; + +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; +use Gedmo\Mapping\Event\ClockAwareAdapterInterface; +use Gedmo\SoftDeleteable\Mapping\Event\SoftDeleteableAdapter; +use Psr\Clock\ClockInterface; + +/** + * Doctrine event adapter for ORM adapted + * for SoftDeleteable behavior. + * + * @author David Buchmann + */ +final class ODM extends BaseAdapterODM implements SoftDeleteableAdapter, ClockAwareAdapterInterface +{ + private ?ClockInterface $clock = null; + + public function setClock(ClockInterface $clock): void + { + $this->clock = $clock; + } + + /** + * @param ClassMetadata $meta + */ + public function getDateValue($meta, $field) + { + $datetime = $this->clock instanceof ClockInterface ? $this->clock->now() : new \DateTimeImmutable(); + $mapping = $meta->getFieldMapping($field); + $type = $mapping['type'] ?? null; + + if ('timestamp' === $type) { + return (int) $datetime->format('U'); + } + + if (in_array($type, ['date_immutable', 'time_immutable', 'datetime_immutable', 'datetimetz_immutable'], true)) { + return $datetime; + } + + return \DateTime::createFromImmutable($datetime); + } +} diff --git a/src/SoftDeleteable/Mapping/Event/Adapter/ORM.php b/src/SoftDeleteable/Mapping/Event/Adapter/ORM.php new file mode 100644 index 0000000000..e20b7f1b70 --- /dev/null +++ b/src/SoftDeleteable/Mapping/Event/Adapter/ORM.php @@ -0,0 +1,70 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Mapping\Event\Adapter; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\FieldMapping; +use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; +use Gedmo\Mapping\Event\ClockAwareAdapterInterface; +use Gedmo\SoftDeleteable\Mapping\Event\SoftDeleteableAdapter; +use Psr\Clock\ClockInterface; + +/** + * Doctrine event adapter for ORM adapted + * for SoftDeleteable behavior. + * + * @author David Buchmann + */ +final class ORM extends BaseAdapterORM implements SoftDeleteableAdapter, ClockAwareAdapterInterface +{ + private ?ClockInterface $clock = null; + + public function setClock(ClockInterface $clock): void + { + $this->clock = $clock; + } + + /** + * @param ClassMetadata $meta + */ + public function getDateValue($meta, $field) + { + $mapping = $meta->getFieldMapping($field); + + return $this->getObjectManager()->getConnection()->convertToPHPValue( + $this->getRawDateValue($mapping), + $mapping instanceof FieldMapping ? $mapping->type : ($mapping['type'] ?? Types::DATETIME_MUTABLE) + ); + } + + /** + * Generates current timestamp for the specified mapping + * + * @param array|FieldMapping $mapping + * + * @return \DateTimeInterface|int + */ + private function getRawDateValue($mapping) + { + $datetime = $this->clock instanceof ClockInterface ? $this->clock->now() : new \DateTimeImmutable(); + $type = $mapping instanceof FieldMapping ? $mapping->type : ($mapping['type'] ?? ''); + + if ('integer' === $type) { + return (int) $datetime->format('U'); + } + + if (in_array($type, ['date_immutable', 'time_immutable', 'datetime_immutable', 'datetimetz_immutable'], true)) { + return $datetime; + } + + return \DateTime::createFromImmutable($datetime); + } +} diff --git a/src/SoftDeleteable/Mapping/Event/SoftDeleteableAdapter.php b/src/SoftDeleteable/Mapping/Event/SoftDeleteableAdapter.php new file mode 100644 index 0000000000..18ba3f7125 --- /dev/null +++ b/src/SoftDeleteable/Mapping/Event/SoftDeleteableAdapter.php @@ -0,0 +1,31 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Mapping\Event; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Mapping\Event\AdapterInterface; + +/** + * Doctrine event adapter for the SoftDeleteable extension. + * + * @author Gediminas Morkevicius + */ +interface SoftDeleteableAdapter extends AdapterInterface +{ + /** + * Get the date value. + * + * @param ClassMetadata $meta + * @param string $field + * + * @return int|\DateTimeInterface + */ + public function getDateValue($meta, $field); +} diff --git a/src/SoftDeleteable/Mapping/Validator.php b/src/SoftDeleteable/Mapping/Validator.php new file mode 100644 index 0000000000..fe896cb32c --- /dev/null +++ b/src/SoftDeleteable/Mapping/Validator.php @@ -0,0 +1,60 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Mapping; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Exception\InvalidMappingException; + +/** + * This class is used to validate mapping information + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class Validator +{ + /** + * List of types which are valid for timestamp + * + * @var string[] + */ + public static $validTypes = [ + 'date', + 'date_immutable', + 'time', + 'time_immutable', + 'datetime', + 'datetime_immutable', + 'datetimetz', + 'datetimetz_immutable', + 'timestamp', + ]; + + /** + * @param ClassMetadata $meta + * @param mixed $field + * + * @return void + */ + public static function validateField(ClassMetadata $meta, $field) + { + if ($meta->isMappedSuperclass) { + return; + } + + $fieldMapping = $meta->getFieldMapping($field); + + if (!in_array($fieldMapping->type ?? $fieldMapping['type'], self::$validTypes, true)) { + throw new InvalidMappingException(sprintf('Field "%s" (type "%s") must be of one of the following types: "%s" in entity %s', $field, $fieldMapping->type ?? $fieldMapping['type'], implode(', ', self::$validTypes), $meta->getName())); + } + } +} diff --git a/src/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php b/src/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php new file mode 100644 index 0000000000..fbe7935fd5 --- /dev/null +++ b/src/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php @@ -0,0 +1,64 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Query\TreeWalker\Exec; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query\AST\Node; +use Doctrine\ORM\Query\Exec\MultiTableDeleteExecutor as BaseMultiTableDeleteExecutor; + +/** + * This class is used when a DELETE DQL query is called for entities + * that are part of an inheritance tree + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class MultiTableDeleteExecutor extends BaseMultiTableDeleteExecutor +{ + /** + * @param ClassMetadata $meta + * @param array $config + */ + public function __construct(Node $AST, $sqlWalker, ClassMetadata $meta, AbstractPlatform $platform, array $config) + { + parent::__construct($AST, $sqlWalker); + + $sqlStatements = $this->getSqlStatements(); + + $quoteStrategy = $sqlWalker->getEntityManager()->getConfiguration()->getQuoteStrategy(); + + foreach ($sqlStatements as $index => $stmt) { + $matches = []; + preg_match('/DELETE FROM (\w+) .+/', $stmt, $matches); + + if (isset($matches[1]) && $quoteStrategy->getTableName($meta, $platform) === $matches[1]) { + $sqlStatements[$index] = str_replace('DELETE FROM', 'UPDATE', $stmt); + $sqlStatements[$index] = str_replace( + 'WHERE', + 'SET '.$config['fieldName'].' = '.$platform->getCurrentTimestampSQL().' WHERE', + $sqlStatements[$index] + ); + } else { + // We have to avoid the removal of registers of child entities of a SoftDeleteable entity + unset($sqlStatements[$index]); + } + } + + // @todo: Once the minimum supported ORM version is 2.17, this can always write to the `$this->sqlStatements` property + if (property_exists($this, 'sqlStatements')) { + $this->sqlStatements = $sqlStatements; + } else { + $this->_sqlStatements = $sqlStatements; + } + } +} diff --git a/src/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php b/src/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php new file mode 100644 index 0000000000..750b41521a --- /dev/null +++ b/src/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php @@ -0,0 +1,211 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Query\TreeWalker; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\QuoteStrategy; +use Doctrine\ORM\Query\AST\DeleteClause; +use Doctrine\ORM\Query\AST\DeleteStatement; +use Doctrine\ORM\Query\AST\SelectStatement; +use Doctrine\ORM\Query\AST\UpdateStatement; +use Doctrine\ORM\Query\Exec\AbstractSqlExecutor; +use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer; +use Doctrine\ORM\Query\Exec\SingleTableDeleteUpdateExecutor; +use Doctrine\ORM\Query\Exec\SqlFinalizer; +use Doctrine\ORM\Query\SqlOutputWalker; +use Gedmo\Exception\RuntimeException; +use Gedmo\Exception\UnexpectedValueException; +use Gedmo\SoftDeleteable\Query\TreeWalker\Exec\MultiTableDeleteExecutor; +use Gedmo\SoftDeleteable\SoftDeleteableListener; +use Gedmo\Tool\ORM\Walker\SqlWalkerCompat; + +/** + * This SqlWalker is needed when you need to use a DELETE DQL query. + * It will update the "deletedAt" field with the actual date, instead + * of actually deleting it. + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class SoftDeleteableWalker extends SqlOutputWalker +{ + use SqlWalkerCompat; + + /** + * @var Connection + * + * @deprecated to be removed in 4.0, use the `getConnection()` method instead. + */ + protected $conn; + + /** + * @var AbstractPlatform + * + * @deprecated to be removed in 4.0, fetch the platform from the connection instead + */ + protected $platform; + + /** + * @var SoftDeleteableListener + */ + protected $listener; + + /** + * @var array + */ + protected $configuration; + + /** + * @var string|null + * + * @deprecated to be removed in 4.0, unused + */ + protected $alias; + + /** + * @var string + */ + protected $deletedAtField; + + /** + * @var ClassMetadata + */ + protected $meta; + + private QuoteStrategy $quoteStrategy; + + public function __construct($query, $parserResult, array $queryComponents) + { + parent::__construct($query, $parserResult, $queryComponents); + + $this->conn = $this->getConnection(); + $this->platform = $this->getConnection()->getDatabasePlatform(); + $this->listener = $this->getSoftDeleteableListener(); + $this->quoteStrategy = $this->getEntityManager()->getConfiguration()->getQuoteStrategy(); + + $this->extractComponents($this->getQueryComponents()); + } + + /** + * @param SelectStatement|UpdateStatement|DeleteStatement $statement + * + * @throws UnexpectedValueException when an unsupported AST statement is given + * + * @phpstan-assert DeleteStatement $statement + */ + protected function doGetExecutorWithCompat($statement): AbstractSqlExecutor + { + if (!$statement instanceof DeleteStatement) { + throw new UnexpectedValueException('SoftDeleteable walker should be used only on delete statement'); + } + + return $this->createDeleteStatementExecutor($statement); + } + + /** + * @param DeleteStatement|UpdateStatement|SelectStatement $AST + * + * @throws UnexpectedValueException when an unsupported AST statement is given + * + * @phpstan-assert DeleteStatement $AST + */ + protected function doGetFinalizerWithCompat($AST): SqlFinalizer + { + if (!$AST instanceof DeleteStatement) { + throw new UnexpectedValueException('SoftDeleteable walker should be used only on delete statement'); + } + + return new PreparedExecutorFinalizer($this->createDeleteStatementExecutor($AST)); + } + + protected function createDeleteStatementExecutor(DeleteStatement $AST): AbstractSqlExecutor + { + assert(class_exists($AST->deleteClause->abstractSchemaName)); + + $primaryClass = $this->getEntityManager()->getClassMetadata($AST->deleteClause->abstractSchemaName); + + return $primaryClass->isInheritanceTypeJoined() + ? new MultiTableDeleteExecutor($AST, $this, $this->meta, $this->getConnection()->getDatabasePlatform(), $this->configuration) + : new SingleTableDeleteUpdateExecutor($AST, $this); + } + + /** + * Changes a DELETE clause into an UPDATE clause for a soft-deleteable entity. + */ + protected function doWalkDeleteClauseWithCompat(DeleteClause $deleteClause): string + { + $em = $this->getEntityManager(); + + assert(class_exists($deleteClause->abstractSchemaName)); + + $class = $em->getClassMetadata($deleteClause->abstractSchemaName); + $tableName = $class->getTableName(); + $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable); + + $platform = $this->getConnection()->getDatabasePlatform(); + + $quotedTableName = $this->quoteStrategy->getTableName($class, $platform); + $quotedColumnName = $this->quoteStrategy->getColumnName($this->deletedAtField, $class, $platform); + + return 'UPDATE '.$quotedTableName.' SET '.$quotedColumnName.' = '.$platform->getCurrentTimestampSQL(); + } + + /** + * Get the currently used SoftDeleteableListener + * + * @throws RuntimeException if listener is not found + */ + private function getSoftDeleteableListener(): SoftDeleteableListener + { + if (null === $this->listener) { + $em = $this->getEntityManager(); + + foreach ($em->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { + if ($listener instanceof SoftDeleteableListener) { + $this->listener = $listener; + + break 2; + } + } + } + + if (null === $this->listener) { + throw new RuntimeException('The SoftDeleteable listener could not be found.'); + } + } + + return $this->listener; + } + + /** + * Search for components in the delete clause + * + * @param array> $queryComponents + */ + private function extractComponents(array $queryComponents): void + { + $em = $this->getEntityManager(); + + foreach ($queryComponents as $comp) { + $meta = $comp['metadata']; + $config = $this->listener->getConfiguration($em, $meta->getName()); + if ($config && isset($config['softDeleteable']) && $config['softDeleteable']) { + $this->configuration = $config; + $this->deletedAtField = $config['fieldName']; + $this->meta = $meta; + } + } + } +} diff --git a/src/SoftDeleteable/SoftDeleteable.php b/src/SoftDeleteable/SoftDeleteable.php new file mode 100644 index 0000000000..0b6d404b3f --- /dev/null +++ b/src/SoftDeleteable/SoftDeleteable.php @@ -0,0 +1,31 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable; + +/** + * Marker interface for objects which can be identified as soft-deletable. + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +interface SoftDeleteable +{ + // this interface is not necessary to implement + + /* + * @Gedmo\SoftDeleteable + * to mark the class as SoftDeleteable use class annotation @Gedmo\SoftDeleteable + * this object will be able to be soft deleted + * example: + * + * @Gedmo\SoftDeleteable + * class MyEntity + */ +} diff --git a/src/SoftDeleteable/SoftDeleteableListener.php b/src/SoftDeleteable/SoftDeleteableListener.php new file mode 100644 index 0000000000..10e7e7f070 --- /dev/null +++ b/src/SoftDeleteable/SoftDeleteableListener.php @@ -0,0 +1,240 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable; + +use Doctrine\Common\EventArgs; +use Doctrine\Common\EventManager; +use Doctrine\Deprecations\Deprecation; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\UnitOfWork as MongoDBUnitOfWork; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\LifecycleEventArgs; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Event\ManagerEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Mapping\MappedEventSubscriber; +use Gedmo\SoftDeleteable\Event\PostSoftDeleteEventArgs; +use Gedmo\SoftDeleteable\Event\PreSoftDeleteEventArgs; +use Gedmo\SoftDeleteable\Mapping\Event\SoftDeleteableAdapter; + +/** + * SoftDeleteable listener + * + * @phpstan-extends MappedEventSubscriber + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class SoftDeleteableListener extends MappedEventSubscriber +{ + /** + * Pre soft-delete event + * + * @var string + */ + public const PRE_SOFT_DELETE = 'preSoftDelete'; + + /** + * Post soft-delete event + * + * @var string + */ + public const POST_SOFT_DELETE = 'postSoftDelete'; + + /** + * Whether the postFlush event should be handled. + */ + private bool $handlePostFlushEvent; + + /** + * Objects soft-deleted on flush. + * + * @var array + */ + private array $softDeletedObjects = []; + + public function __construct(bool $handlePostFlushEvent = false) + { + parent::__construct(); + + $this->handlePostFlushEvent = $handlePostFlushEvent; + } + + /** + * @return string[] + */ + public function getSubscribedEvents() + { + return [ + 'loadClassMetadata', + 'onFlush', + 'postFlush', + ]; + } + + /** + * If it's a SoftDeleteable object, update the "deletedAt" field + * and skip the removal of the object + * + * @param ManagerEventArgs $args + * + * @phpstan-param ManagerEventArgs $args + * + * @return void + */ + public function onFlush(EventArgs $args) + { + $ea = $this->getEventAdapter($args); + /** @var EntityManagerInterface|DocumentManager $om */ + $om = $ea->getObjectManager(); + $uow = $om->getUnitOfWork(); + $evm = $om->getEventManager(); + + // getScheduledDocumentDeletions + foreach ($ea->getScheduledObjectDeletions($uow) as $object) { + $meta = $om->getClassMetadata(get_class($object)); + $config = $this->getConfiguration($om, $meta->getName()); + + if (isset($config['softDeleteable']) && $config['softDeleteable']) { + $oldValue = $meta->getFieldValue($object, $config['fieldName']); + $date = $ea->getDateValue($meta, $config['fieldName']); + + if (isset($config['hardDelete']) && $config['hardDelete'] && $oldValue instanceof \DateTimeInterface && $oldValue <= $date) { + continue; // want to hard delete + } + + if ($evm->hasListeners(self::PRE_SOFT_DELETE)) { + // @todo: in the next major remove check and only instantiate the event + $preSoftDeleteEventArgs = $this->hasToDispatchNewEvent($om, $evm, self::PRE_SOFT_DELETE, PreSoftDeleteEventArgs::class) + ? new PreSoftDeleteEventArgs($object, $om) + : $ea->createLifecycleEventArgsInstance($object, $om); + + $evm->dispatchEvent( + self::PRE_SOFT_DELETE, + $preSoftDeleteEventArgs, + ); + } + + $meta->setFieldValue($object, $config['fieldName'], $date); + + $om->persist($object); + $uow->propertyChanged($object, $config['fieldName'], $oldValue, $date); + if ($uow instanceof MongoDBUnitOfWork) { + $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); + } else { + $uow->scheduleExtraUpdate($object, [ + $config['fieldName'] => [$oldValue, $date], + ]); + } + + if ($evm->hasListeners(self::POST_SOFT_DELETE)) { + // @todo: in the next major remove check and only instantiate the event + $postSoftDeleteEventArgs = $this->hasToDispatchNewEvent($om, $evm, self::POST_SOFT_DELETE, PostSoftDeleteEventArgs::class) + ? new PostSoftDeleteEventArgs($object, $om) + : $ea->createLifecycleEventArgsInstance($object, $om); + + $evm->dispatchEvent( + self::POST_SOFT_DELETE, + $postSoftDeleteEventArgs + ); + } + + if ($this->handlePostFlushEvent) { + $this->softDeletedObjects[] = $object; + } + } + } + } + + /** + * Detach soft-deleted objects from object manager. + * + * @return void + */ + public function postFlush(EventArgs $args) + { + if (!$this->handlePostFlushEvent) { + return; + } + + $ea = $this->getEventAdapter($args); + $om = $ea->getObjectManager(); + foreach ($this->softDeletedObjects as $index => $object) { + $om->detach($object); + unset($this->softDeletedObjects[$index]); + } + } + + /** + * Maps additional metadata + * + * @param LoadClassMetadataEventArgs $eventArgs + * + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $eventArgs + * + * @return void + */ + public function loadClassMetadata(EventArgs $eventArgs) + { + $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); + } + + public function setHandlePostFlushEvent(bool $handlePostFlushEvent): void + { + $this->handlePostFlushEvent = $handlePostFlushEvent; + } + + public function shouldHandlePostFlushEvent(): bool + { + return $this->handlePostFlushEvent; + } + + protected function getNamespace() + { + return __NAMESPACE__; + } + + /** @param class-string $eventClass */ + private function hasToDispatchNewEvent(ObjectManager $objectManager, EventManager $eventManager, string $eventName, string $eventClass): bool + { + if ($objectManager instanceof EntityManagerInterface && !class_exists(LifecycleEventArgs::class)) { + return true; + } + + foreach ($eventManager->getListeners($eventName) as $listener) { + $reflMethod = new \ReflectionMethod($listener, $eventName); + + $parameters = $reflMethod->getParameters(); + + if ( + 1 !== count($parameters) + || !$parameters[0]->hasType() + || !$parameters[0]->getType() instanceof \ReflectionNamedType + || $eventClass !== $parameters[0]->getType()->getName() + ) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2649', + 'Type-hinting to something different than "%s" in "%s::%s()" is deprecated.', + $eventClass, + get_class($listener), + $reflMethod->getName() + ); + + return false; + } + } + + return true; + } +} diff --git a/src/SoftDeleteable/Traits/SoftDeleteable.php b/src/SoftDeleteable/Traits/SoftDeleteable.php new file mode 100644 index 0000000000..b11604a6e1 --- /dev/null +++ b/src/SoftDeleteable/Traits/SoftDeleteable.php @@ -0,0 +1,58 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Traits; + +/** + * Trait for soft-deletable objects. + * + * This implementation does not provide any mapping configurations. + * + * @author Wesley van Opdorp + */ +trait SoftDeleteable +{ + /** + * @var \DateTime|null + */ + protected $deletedAt; + + /** + * Set or clear the deleted at timestamp. + * + * @return self + */ + public function setDeletedAt(?\DateTime $deletedAt = null) + { + $this->deletedAt = $deletedAt; + + return $this; + } + + /** + * Get the deleted at timestamp value. Will return null if + * the entity has not been soft deleted. + * + * @return \DateTime|null + */ + public function getDeletedAt() + { + return $this->deletedAt; + } + + /** + * Check if the entity has been soft deleted. + * + * @return bool + */ + public function isDeleted() + { + return null !== $this->deletedAt; + } +} diff --git a/src/SoftDeleteable/Traits/SoftDeleteableDocument.php b/src/SoftDeleteable/Traits/SoftDeleteableDocument.php new file mode 100644 index 0000000000..310ca92462 --- /dev/null +++ b/src/SoftDeleteable/Traits/SoftDeleteableDocument.php @@ -0,0 +1,64 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Traits; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; + +/** + * Trait for soft-deletable objects. + * + * This implementation provides a mapping configuration for the Doctrine MongoDB ODM. + * + * @author Wesley van Opdorp + */ +trait SoftDeleteableDocument +{ + /** + * @ODM\Field(type="date") + * + * @var \DateTime|null + */ + #[ODM\Field(type: Type::DATE)] + protected $deletedAt; + + /** + * Set or clear the deleted at timestamp. + * + * @return self + */ + public function setDeletedAt(?\DateTime $deletedAt = null) + { + $this->deletedAt = $deletedAt; + + return $this; + } + + /** + * Get the deleted at timestamp value. Will return null if + * the entity has not been soft deleted. + * + * @return \DateTime|null + */ + public function getDeletedAt() + { + return $this->deletedAt; + } + + /** + * Check if the entity has been soft deleted. + * + * @return bool + */ + public function isDeleted() + { + return null !== $this->deletedAt; + } +} diff --git a/src/SoftDeleteable/Traits/SoftDeleteableEntity.php b/src/SoftDeleteable/Traits/SoftDeleteableEntity.php new file mode 100644 index 0000000000..bd329ac9db --- /dev/null +++ b/src/SoftDeleteable/Traits/SoftDeleteableEntity.php @@ -0,0 +1,65 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\SoftDeleteable\Traits; + +use DateTime; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +/** + * Trait for soft-deletable objects. + * + * This implementation provides a mapping configuration for the Doctrine ORM. + * + * @author Wesley van Opdorp + */ +trait SoftDeleteableEntity +{ + /** + * @ORM\Column(type="datetime", nullable=true) + * + * @var \DateTime|null + */ + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + protected $deletedAt; + + /** + * Set or clear the deleted at timestamp. + * + * @return self + */ + public function setDeletedAt(?\DateTime $deletedAt = null) + { + $this->deletedAt = $deletedAt; + + return $this; + } + + /** + * Get the deleted at timestamp value. Will return null if + * the entity has not been soft deleted. + * + * @return \DateTime|null + */ + public function getDeletedAt() + { + return $this->deletedAt; + } + + /** + * Check if the entity has been soft deleted. + * + * @return bool + */ + public function isDeleted() + { + return null !== $this->deletedAt; + } +} diff --git a/src/Sortable/Entity/Repository/SortableRepository.php b/src/Sortable/Entity/Repository/SortableRepository.php new file mode 100644 index 0000000000..2a475d556c --- /dev/null +++ b/src/Sortable/Entity/Repository/SortableRepository.php @@ -0,0 +1,125 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sortable\Entity\Repository; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query; +use Doctrine\ORM\QueryBuilder; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Sortable\SortableListener; + +/** + * Sortable Repository + * + * @author Lukas Botsch + * + * @template T of object + * + * @template-extends EntityRepository + */ +class SortableRepository extends EntityRepository +{ + /** + * Sortable listener on event manager + * + * @var SortableListener + */ + protected $listener; + + /** + * @var array + */ + protected $config; + + /** + * @var ClassMetadata + */ + protected $meta; + + /** + * @param ClassMetadata $class + */ + public function __construct(EntityManagerInterface $em, ClassMetadata $class) + { + parent::__construct($em, $class); + $sortableListener = null; + foreach ($em->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { + if ($listener instanceof SortableListener) { + $sortableListener = $listener; + + break 2; + } + } + } + + if (null === $sortableListener) { + throw new InvalidMappingException('This repository can be attached only to ORM sortable listener'); + } + + $this->listener = $sortableListener; + $this->meta = $this->getClassMetadata(); + $this->config = $this->listener->getConfiguration($this->getEntityManager(), $this->meta->getName()); + } + + /** + * @param array $groupValues + * + * @return Query + */ + public function getBySortableGroupsQuery(array $groupValues = []) + { + return $this->getBySortableGroupsQueryBuilder($groupValues)->getQuery(); + } + + /** + * @param array $groupValues + * + * @return QueryBuilder + */ + public function getBySortableGroupsQueryBuilder(array $groupValues = []) + { + $groups = isset($this->config['groups']) ? array_combine(array_values($this->config['groups']), array_keys($this->config['groups'])) : []; + foreach ($groupValues as $name => $value) { + if (!in_array($name, $this->config['groups'], true)) { + throw new \InvalidArgumentException('Sortable group "'.$name.'" is not defined in Entity '.$this->meta->getName()); + } + unset($groups[$name]); + } + if ([] !== $groups) { + throw new \InvalidArgumentException('You need to specify values for the following groups to select by sortable groups: '.implode(', ', array_keys($groups))); + } + + $qb = $this->createQueryBuilder('n'); + $qb->orderBy('n.'.$this->config['position']); + $i = 1; + foreach ($groupValues as $group => $value) { + $qb->andWhere('n.'.$group.' = :group'.$i) + ->setParameter('group'.$i, $value); + ++$i; + } + + return $qb; + } + + /** + * @param array $groupValues + * + * @return array + */ + public function getBySortableGroups(array $groupValues = []) + { + $query = $this->getBySortableGroupsQuery($groupValues); + + return $query->getResult(); + } +} diff --git a/src/Sortable/Mapping/Driver/Annotation.php b/src/Sortable/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..a1367c4c06 --- /dev/null +++ b/src/Sortable/Mapping/Driver/Annotation.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sortable\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the sortable extension which reads extended metadata from annotations on a sortable class. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/lib/Gedmo/Sortable/Mapping/Driver/Annotation.php b/src/Sortable/Mapping/Driver/Attribute.php similarity index 62% rename from lib/Gedmo/Sortable/Mapping/Driver/Annotation.php rename to src/Sortable/Mapping/Driver/Attribute.php index c277a26db7..61ac71e06c 100644 --- a/lib/Gedmo/Sortable/Mapping/Driver/Annotation.php +++ b/src/Sortable/Mapping/Driver/Attribute.php @@ -1,55 +1,61 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sortable\Mapping\Driver; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\SortableGroup; +use Gedmo\Mapping\Annotation\SortablePosition; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an annotation mapping driver for Sortable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Sortable - * extension. + * Mapping driver for the sortable extension which reads extended metadata from attributes on a sortable class. * * @author Lukas Botsch + * * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ -class Annotation extends AbstractAnnotationDriver +class Attribute extends AbstractAnnotationDriver { /** - * Annotation to mark field as one which will store node position + * Mapping object to mark a field as the one which will store the node position on a sortable object. */ - const POSITION = 'Gedmo\\Mapping\\Annotation\\SortablePosition'; + public const POSITION = SortablePosition::class; /** - * Annotation to mark field as sorting group + * Mapping object to mark a field as part of a sorting group for a sortable object. */ - const GROUP = 'Gedmo\\Mapping\\Annotation\\SortableGroup'; + public const GROUP = SortableGroup::class; /** * List of types which are valid for position fields * - * @var array + * @var string[] */ - protected $validTypes = array( + protected $validTypes = [ 'int', 'integer', 'smallint', 'bigint', - ); + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { $class = $this->getMetaReflectionClass($meta); // property annotations foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() || - $meta->isInheritedField($property->name) || - isset($meta->associationMappings[$property->name]['inherited']) + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) ) { continue; } @@ -57,32 +63,37 @@ public function readExtendedMetadata($meta, array &$config) // position if ($this->reader->getPropertyAnnotation($property, self::POSITION)) { $field = $property->getName(); + if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'position' - [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find 'position' - [{$field}] as mapped property in entity - {$meta->getName()}"); } + if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Sortable position field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Sortable position field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } + $config['position'] = $field; } // group if ($this->reader->getPropertyAnnotation($property, self::GROUP)) { $field = $property->getName(); + if (!$meta->hasField($field) && !$meta->hasAssociation($field)) { - throw new InvalidMappingException("Unable to find 'group' - [{$field}] as mapped property in entity - {$meta->name}"); - } - if (!isset($config['groups'])) { - $config['groups'] = array(); + throw new InvalidMappingException("Unable to find 'group' - [{$field}] as mapped property in entity - {$meta->getName()}"); } + + $config['groups'] ??= []; $config['groups'][] = $field; } } if (!$meta->isMappedSuperclass && $config) { if (!isset($config['position'])) { - throw new InvalidMappingException("Missing property: 'position' in class - {$meta->name}"); + throw new InvalidMappingException("Missing property: 'position' in class - {$meta->getName()}"); } } + + return $config; } } diff --git a/lib/Gedmo/Sortable/Mapping/Driver/Xml.php b/src/Sortable/Mapping/Driver/Xml.php similarity index 63% rename from lib/Gedmo/Sortable/Mapping/Driver/Xml.php rename to src/Sortable/Mapping/Driver/Xml.php index 4675aa03b1..3cdc6006d7 100644 --- a/lib/Gedmo/Sortable/Mapping/Driver/Xml.php +++ b/src/Sortable/Mapping/Driver/Xml.php @@ -1,9 +1,17 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sortable\Mapping\Driver; -use Gedmo\Mapping\Driver\Xml as BaseXml; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for Sortable @@ -12,31 +20,29 @@ * extension. * * @author Lukas Botsch - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ class Xml extends BaseXml { /** * List of types which are valid for position field * - * @var array + * @var string[] */ - private $validTypes = array( + private const VALID_TYPES = [ 'int', 'integer', 'smallint', 'bigint', - ); + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { /** - * @var \SimpleXmlElement $xml + * @var \SimpleXmlElement */ - $xml = $this->_getMapping($meta->name); + $xml = $this->_getMapping($meta->getName()); if (isset($xml->field)) { foreach ($xml->field as $mappingDoctrine) { @@ -45,37 +51,54 @@ public function readExtendedMetadata($meta, array &$config) $field = $this->_getAttribute($mappingDoctrine, 'name'); if (isset($mapping->{'sortable-position'})) { if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Sortable position field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Sortable position field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } $config['position'] = $field; } } - $this->readSortableGroups($xml->field, $config, 'name'); + $config = $this->readSortableGroups($xml->field, $config, 'name'); } // Search for sortable-groups in association mappings if (isset($xml->{'many-to-one'})) { - $this->readSortableGroups($xml->{'many-to-one'}, $config); + $config = $this->readSortableGroups($xml->{'many-to-one'}, $config); } // Search for sortable-groups in association mappings if (isset($xml->{'many-to-many'})) { - $this->readSortableGroups($xml->{'many-to-many'}, $config); + $config = $this->readSortableGroups($xml->{'many-to-many'}, $config); } if (!$meta->isMappedSuperclass && $config) { if (!isset($config['position'])) { - throw new InvalidMappingException("Missing property: 'position' in class - {$meta->name}"); + throw new InvalidMappingException("Missing property: 'position' in class - {$meta->getName()}"); } } + + return $config; + } + + /** + * Checks if $field type is valid as Sortable Position field + * + * @param ClassMetadata $meta + * @param string $field + * + * @return bool + */ + protected function isValidField($meta, $field) + { + $mapping = $meta->getFieldMapping($field); + + return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true); } /** - * @param \SimpleXMLElement[] $mapping - * @param array $config - * @param string $fieldAttr + * @param array $config + * + * @return array */ - private function readSortableGroups($mapping, array &$config, $fieldAttr = 'field') + private function readSortableGroups(\SimpleXMLElement $mapping, array $config, string $fieldAttr = 'field'): array { foreach ($mapping as $mappingDoctrine) { $map = $mappingDoctrine->children(self::GEDMO_NAMESPACE_URI); @@ -83,25 +106,12 @@ private function readSortableGroups($mapping, array &$config, $fieldAttr = 'fiel $field = $this->_getAttribute($mappingDoctrine, $fieldAttr); if (isset($map->{'sortable-group'})) { if (!isset($config['groups'])) { - $config['groups'] = array(); + $config['groups'] = []; } $config['groups'][] = $field; } } - } - - /** - * Checks if $field type is valid as Sortable Position field - * - * @param object $meta - * @param string $field - * - * @return boolean - */ - protected function isValidField($meta, $field) - { - $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validTypes); + return $config; } } diff --git a/lib/Gedmo/Sortable/Mapping/Driver/Yaml.php b/src/Sortable/Mapping/Driver/Yaml.php similarity index 61% rename from lib/Gedmo/Sortable/Mapping/Driver/Yaml.php rename to src/Sortable/Mapping/Driver/Yaml.php index e47000a9ac..86a2ca39a0 100644 --- a/lib/Gedmo/Sortable/Mapping/Driver/Yaml.php +++ b/src/Sortable/Mapping/Driver/Yaml.php @@ -1,10 +1,18 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sortable\Mapping\Driver; -use Gedmo\Mapping\Driver\File; -use Gedmo\Mapping\Driver; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Sortable @@ -13,79 +21,65 @@ * extension. * * @author Lukas Botsch - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ class Yaml extends File implements Driver { - /** - * File extension - * @var string - */ - protected $_extension = '.dcm.yml'; - /** * List of types which are valid for position fields * - * @var array + * @var string[] */ - private $validTypes = array( + private const VALID_TYPES = [ 'int', 'integer', 'smallint', 'bigint', - ); + ]; /** - * {@inheritDoc} + * File extension + * + * @var string */ + protected $_extension = '.dcm.yml'; + public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { - if (in_array('sortablePosition', $fieldMapping['gedmo'])) { + if (in_array('sortablePosition', $fieldMapping['gedmo'], true)) { if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Sortable position field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Sortable position field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } $config['position'] = $field; } } } - $this->readSortableGroups($mapping['fields'], $config); + $config = $this->readSortableGroups($mapping['fields'], $config); } if (isset($mapping['manyToOne'])) { - $this->readSortableGroups($mapping['manyToOne'], $config); + $config = $this->readSortableGroups($mapping['manyToOne'], $config); } if (isset($mapping['manyToMany'])) { - $this->readSortableGroups($mapping['manyToMany'], $config); + $config = $this->readSortableGroups($mapping['manyToMany'], $config); } if (!$meta->isMappedSuperclass && $config) { if (!isset($config['position'])) { - throw new InvalidMappingException("Missing property: 'position' in class - {$meta->name}"); + throw new InvalidMappingException("Missing property: 'position' in class - {$meta->getName()}"); } } - } - private function readSortableGroups($mapping, array &$config) - { - foreach ($mapping as $field => $fieldMapping) { - if (isset($fieldMapping['gedmo'])) { - if (in_array('sortableGroup', $fieldMapping['gedmo'])) { - if (!isset($config['groups'])) { - $config['groups'] = array(); - } - $config['groups'][] = $field; - } - } - } + return $config; } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); @@ -94,15 +88,37 @@ protected function _loadMappingFile($file) /** * Checks if $field type is valid as SortablePosition field * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validTypes); + return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true); + } + + /** + * @param iterable> $mapping + * @param array $config + * + * @return array + */ + private function readSortableGroups(iterable $mapping, array $config): array + { + foreach ($mapping as $field => $fieldMapping) { + if (isset($fieldMapping['gedmo'])) { + if (in_array('sortableGroup', $fieldMapping['gedmo'], true)) { + if (!isset($config['groups'])) { + $config['groups'] = []; + } + $config['groups'][] = $field; + } + } + } + + return $config; } } diff --git a/lib/Gedmo/Sortable/Mapping/Event/Adapter/ODM.php b/src/Sortable/Mapping/Event/Adapter/ODM.php similarity index 61% rename from lib/Gedmo/Sortable/Mapping/Event/Adapter/ODM.php rename to src/Sortable/Mapping/Event/Adapter/ODM.php index 773a8373c5..7484058c44 100644 --- a/lib/Gedmo/Sortable/Mapping/Event/Adapter/ODM.php +++ b/src/Sortable/Mapping/Event/Adapter/ODM.php @@ -1,20 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sortable\Mapping\Event\Adapter; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\Sortable\Mapping\Event\SortableAdapter; -use Doctrine\Common\Util\ClassUtils; +use Gedmo\Sortable\SortableListener; +use Gedmo\Tool\ClassUtils; /** * Doctrine event adapter for ODM adapted * for sortable behavior * * @author Lukas Botsch - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @phpstan-import-type SortableConfiguration from SortableListener + * @phpstan-import-type SortableRelocation from SortableListener */ final class ODM extends BaseAdapterODM implements SortableAdapter { + /** + * @param array $config + * @param ClassMetadata $meta + * @param iterable $groups + * + * @phpstan-param SortableConfiguration $config + * + * @return int + */ public function getMaxPosition(array $config, $meta, $groups) { $dm = $this->getObjectManager(); @@ -31,12 +51,22 @@ public function getMaxPosition(array $config, $meta, $groups) $document = $qb->getQuery()->getSingleResult(); if ($document) { - return $meta->getReflectionProperty($config['position'])->getValue($document); + return $meta->getFieldValue($document, $config['position']); } return -1; } + /** + * @param array $relocation + * @param array $delta + * @param array $config + * + * @phpstan-param SortableRelocation $relocation + * @phpstan-param SortableConfiguration $config + * + * @return void + */ public function updatePositions($relocation, $delta, $config) { $dm = $this->getObjectManager(); @@ -44,8 +74,7 @@ public function updatePositions($relocation, $delta, $config) $delta = array_map('intval', $delta); $qb = $dm->createQueryBuilder($config['useObjectClass']); - $qb->update(); - $qb->multiple(true); + $qb->updateMany(); $qb->field($config['position'])->inc($delta['delta']); $qb->field($config['position'])->gte($delta['start']); if ($delta['stop'] > 0) { diff --git a/lib/Gedmo/Sortable/Mapping/Event/Adapter/ORM.php b/src/Sortable/Mapping/Event/Adapter/ORM.php similarity index 56% rename from lib/Gedmo/Sortable/Mapping/Event/Adapter/ORM.php rename to src/Sortable/Mapping/Event/Adapter/ORM.php index 82723481f4..4331ffbe57 100644 --- a/lib/Gedmo/Sortable/Mapping/Event/Adapter/ORM.php +++ b/src/Sortable/Mapping/Event/Adapter/ORM.php @@ -1,20 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sortable\Mapping\Event\Adapter; +use Doctrine\ORM\QueryBuilder; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\Sortable\Mapping\Event\SortableAdapter; -use Doctrine\ORM\QueryBuilder; +use Gedmo\Sortable\SortableListener; /** * Doctrine event adapter for ORM adapted * for sortable behavior * * @author Lukas Botsch - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @phpstan-import-type SortableConfiguration from SortableListener + * @phpstan-import-type SortableRelocation from SortableListener */ final class ORM extends BaseAdapterORM implements SortableAdapter { + /** + * @param array $config + * @param ClassMetadata $meta + * @param iterable $groups + * + * @phpstan-param SortableConfiguration $config + * + * @return int|null + */ public function getMaxPosition(array $config, $meta, $groups) { $em = $this->getObjectManager(); @@ -22,32 +42,28 @@ public function getMaxPosition(array $config, $meta, $groups) $qb = $em->createQueryBuilder(); $qb->select('MAX(n.'.$config['position'].')') ->from($config['useObjectClass'], 'n'); - $this->addGroupWhere($qb, $groups); + $this->addGroupWhere($qb, $meta, $groups); $query = $qb->getQuery(); $query->useQueryCache(false); - $query->useResultCache(false); - $res = $query->getResult(); + $query->disableResultCache(); + $query->setMaxResults(1); - return $res[0][1]; - } - - private function addGroupWhere(QueryBuilder $qb, $groups) - { - $i = 1; - foreach ($groups as $group => $value) { - if (is_null($value)) { - $qb->andWhere($qb->expr()->isNull('n.'.$group)); - } else { - $qb->andWhere('n.'.$group.' = :group__'.$i); - $qb->setParameter('group__'.$i, $value); - } - $i++; - } + return $query->getSingleScalarResult(); } + /** + * @param array $relocation + * @param array $delta + * @param array $config + * + * @phpstan-param SortableRelocation $relocation + * @phpstan-param SortableConfiguration $config + * + * @return void + */ public function updatePositions($relocation, $delta, $config) { - $sign = $delta['delta'] < 0 ? "-" : "+"; + $sign = $delta['delta'] < 0 ? '-' : '+'; $absDelta = abs($delta['delta']); $dql = "UPDATE {$relocation['name']} n"; $dql .= " SET n.{$config['position']} = n.{$config['position']} {$sign} {$absDelta}"; @@ -57,9 +73,9 @@ public function updatePositions($relocation, $delta, $config) $dql .= " AND n.{$config['position']} < {$delta['stop']}"; } $i = -1; - $params = array(); + $params = []; foreach ($relocation['groups'] as $group => $value) { - if (is_null($value)) { + if (null === $value) { $dql .= " AND n.{$group} IS NULL"; } else { $dql .= " AND n.{$group} = :val___".(++$i); @@ -70,28 +86,28 @@ public function updatePositions($relocation, $delta, $config) // add excludes if (!empty($delta['exclude'])) { $meta = $this->getObjectManager()->getClassMetadata($relocation['name']); - if (count($meta->identifier) == 1) { + if (1 === count($meta->getIdentifier())) { // if we only have one identifier, we can use IN syntax, for better performance - $excludedIds = array(); + $excludedIds = []; foreach ($delta['exclude'] as $entity) { - if ($id = $meta->getFieldValue($entity, $meta->identifier[0])) { + if ($id = $meta->getFieldValue($entity, $meta->getIdentifier()[0])) { $excludedIds[] = $id; } } if (!empty($excludedIds)) { $params['excluded'] = $excludedIds; - $dql .= " AND n.{$meta->identifier[0]} NOT IN (:excluded)"; + $dql .= " AND n.{$meta->getIdentifier()[0]} NOT IN (:excluded)"; } - } else if (count($meta->identifier) > 1) { + } elseif (count($meta->getIdentifier()) > 1) { foreach ($delta['exclude'] as $entity) { $j = 0; - $dql .= " AND NOT ("; + $dql .= ' AND NOT ('; foreach ($meta->getIdentifierValues($entity) as $id => $value) { - $dql .= ($j > 0 ? " AND " : "") . "n.{$id} = :val___".(++$i); + $dql .= ($j > 0 ? ' AND ' : '')."n.{$id} = :val___".(++$i); $params['val___'.$i] = $value; - $j++; + ++$j; } - $dql .= ")"; + $dql .= ')'; } } } @@ -101,4 +117,22 @@ public function updatePositions($relocation, $delta, $config) $q->setParameters($params); $q->getSingleScalarResult(); } + + /** + * @param ClassMetadata $metadata + * @param iterable $groups + */ + private function addGroupWhere(QueryBuilder $qb, ClassMetadata $metadata, iterable $groups): void + { + $i = 1; + foreach ($groups as $group => $value) { + if (null === $value) { + $qb->andWhere($qb->expr()->isNull('n.'.$group)); + } else { + $qb->andWhere('n.'.$group.' = :group__'.$i); + $qb->setParameter('group__'.$i, $value, $metadata->getTypeOfField($group)); + } + ++$i; + } + } } diff --git a/src/Sortable/Mapping/Event/SortableAdapter.php b/src/Sortable/Mapping/Event/SortableAdapter.php new file mode 100644 index 0000000000..ecdd8ce979 --- /dev/null +++ b/src/Sortable/Mapping/Event/SortableAdapter.php @@ -0,0 +1,21 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sortable\Mapping\Event; + +use Gedmo\Mapping\Event\AdapterInterface; + +/** + * Doctrine event adapter for the Sortable extension. + * + * @author Lukas Botsch + */ +interface SortableAdapter extends AdapterInterface +{ +} diff --git a/src/Sortable/Sortable.php b/src/Sortable/Sortable.php new file mode 100644 index 0000000000..77cb710abb --- /dev/null +++ b/src/Sortable/Sortable.php @@ -0,0 +1,46 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Sortable; + +/** + * Marker interface for objects which can be identified as sortable. + * + * @author Lukas Botsch + */ +interface Sortable +{ + // use now annotations instead of predefined methods, this interface is not necessary + + /* + * @Gedmo\SortablePosition - to mark property which will hold the item position use annotation @Gedmo\SortablePosition + * This property has to be numeric. The position index can be negative and will be counted from right to left. + * + * example: + * + * @Gedmo\SortablePosition + * @Column(type="int") + * $position + * + * @Gedmo\SortableGroup + * @Column(type="string", length=64) + * $category + * + */ + + /* + * @Gedmo\SortableGroup - to group node sorting by a property use annotation @Gedmo\SortableGroup on this property + * + * example: + * + * @Gedmo\SortableGroup + * @Column(type="string", length=64) + * $category + */ +} diff --git a/lib/Gedmo/Sortable/SortableListener.php b/src/Sortable/SortableListener.php similarity index 57% rename from lib/Gedmo/Sortable/SortableListener.php rename to src/Sortable/SortableListener.php index cb9422153f..d56e958c25 100644 --- a/lib/Gedmo/Sortable/SortableListener.php +++ b/src/Sortable/SortableListener.php @@ -1,13 +1,26 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Sortable; +use Doctrine\Common\Comparable; use Doctrine\Common\EventArgs; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; -use Doctrine\Common\Persistence\Proxy; -use Doctrine\Common\Util\ClassUtils; +use Doctrine\Deprecations\Deprecation; +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Event\ManagerEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\MappedEventSubscriber; use Gedmo\Sortable\Mapping\Event\SortableAdapter; +use Gedmo\Tool\ClassUtils; +use ProxyManager\Proxy\GhostObjectInterface; /** * The SortableListener maintains a sort index on your entities @@ -17,22 +30,49 @@ * since it does some additional calculations on persisted objects. * * @author Lukas Botsch - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @phpstan-type SortableConfiguration = array{ + * groups?: string[], + * position?: string, + * useObjectClass?: class-string, + * } + * @phpstan-type SortableRelocation = array{ + * name?: class-string, + * groups?: mixed[], + * deltas?: array, + * } + * + * @phpstan-extends MappedEventSubscriber + * + * @final since gedmo/doctrine-extensions 3.11 */ class SortableListener extends MappedEventSubscriber { - private $relocations = array(); - private $persistenceNeeded = false; - private $maxPositions = array(); + /** + * @var array> + * + * @phpstan-var array + */ + private array $relocations = []; + + private bool $persistenceNeeded = false; + + /** @var array */ + private array $maxPositions = []; /** * Specifies the list of events to listen * - * @return array + * @return string[] */ public function getSubscribedEvents() { - return array( + return [ 'onFlush', 'loadClassMetadata', 'prePersist', @@ -40,13 +80,17 @@ public function getSubscribedEvents() 'preUpdate', 'postRemove', 'postFlush', - ); + ]; } /** * Maps additional metadata * - * @param EventArgs $args + * @param LoadClassMetadataEventArgs $args + * + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $args + * + * @return void */ public function loadClassMetadata(EventArgs $args) { @@ -64,7 +108,11 @@ public function loadClassMetadata(EventArgs $args) * The synchronization of the objects in memory is done in postFlush. This * ensures that the positions have been successfully persisted to database. * - * @param EventArgs $args + * @param ManagerEventArgs $args + * + * @phpstan-param ManagerEventArgs $args + * + * @return void */ public function onFlush(EventArgs $args) { @@ -77,23 +125,29 @@ public function onFlush(EventArgs $args) // process all objects being deleted foreach ($ea->getScheduledObjectDeletions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - if ($config = $this->getConfiguration($om, $meta->name)) { + if ($config = $this->getConfiguration($om, $meta->getName())) { $this->processDeletion($ea, $config, $meta, $object); } } + $updateValues = []; // process all objects being updated foreach ($ea->getScheduledObjectUpdates($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - if ($config = $this->getConfiguration($om, $meta->name)) { - $this->processUpdate($ea, $config, $meta, $object); + if ($config = $this->getConfiguration($om, $meta->getName())) { + $position = $meta->getFieldValue($object, $config['position']); + $updateValues[$position] = [$ea, $config, $meta, $object]; } } + krsort($updateValues); + foreach ($updateValues as [$ea, $config, $meta, $object]) { + $this->processUpdate($ea, $config, $meta, $object); + } // process all objects being inserted foreach ($ea->getScheduledObjectInsertions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - if ($config = $this->getConfiguration($om, $meta->name)) { + if ($config = $this->getConfiguration($om, $meta->getName())) { $this->processInsert($ea, $config, $meta, $object); } } @@ -102,7 +156,11 @@ public function onFlush(EventArgs $args) /** * Update maxPositions as needed * - * @param EventArgs $args + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void */ public function prePersist(EventArgs $args) { @@ -111,7 +169,7 @@ public function prePersist(EventArgs $args) $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - if ($config = $this->getConfiguration($om, $meta->name)) { + if ($config = $this->getConfiguration($om, $meta->getName())) { // Get groups $groups = $this->getGroups($meta, $config, $object); @@ -125,6 +183,13 @@ public function prePersist(EventArgs $args) } } + /** + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void + */ public function postPersist(EventArgs $args) { // persist position updates here, so that the update queries @@ -132,6 +197,13 @@ public function postPersist(EventArgs $args) $this->persistRelocations($this->getEventAdapter($args)); } + /** + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void + */ public function preUpdate(EventArgs $args) { // persist position updates here, so that the update queries @@ -139,6 +211,13 @@ public function preUpdate(EventArgs $args) $this->persistRelocations($this->getEventAdapter($args)); } + /** + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void + */ public function postRemove(EventArgs $args) { // persist position updates here, so that the update queries @@ -147,22 +226,139 @@ public function postRemove(EventArgs $args) } /** - * Computes node positions and updates the sort field in memory and in the db + * Sync objects in memory + * + * @param ManagerEventArgs $args * - * @param SortableAdapter $ea - * @param array $config - * @param ClassMetadata $meta - * @param object $object + * @phpstan-param ManagerEventArgs $args + * + * @return void */ - private function processInsert(SortableAdapter $ea, array $config, $meta, $object) + public function postFlush(EventArgs $args) { + $ea = $this->getEventAdapter($args); $em = $ea->getObjectManager(); - $uow = $em->getUnitOfWork(); - $old = $meta->getReflectionProperty($config['position'])->getValue($object); - $newPosition = $meta->getReflectionProperty($config['position'])->getValue($object); + $updatedObjects = []; - if (is_null($newPosition)) { + foreach ($this->relocations as $hash => $relocation) { + $config = $this->getConfiguration($em, $relocation['name']); + foreach ($relocation['deltas'] as $delta) { + if ($delta['start'] > $this->maxPositions[$hash] || 0 == $delta['delta']) { + continue; + } + + $meta = $em->getClassMetadata($relocation['name']); + + // now walk through the unit of work in memory objects and sync those + $uow = $em->getUnitOfWork(); + foreach ($uow->getIdentityMap() as $className => $objects) { + // for inheritance mapped classes, only root is always in the identity map + if ($className !== $ea->getRootObjectClass($meta) || !$this->getConfiguration($em, $className)) { + continue; + } + foreach ($objects as $object) { + if ($object instanceof GhostObjectInterface && !$object->isProxyInitialized()) { + continue; + } + + $changeSet = $ea->getObjectChangeSet($uow, $object); + + // if the entity's position is already changed, stop now + if (array_key_exists($config['position'], $changeSet)) { + continue; + } + + // if the entity's group has changed, we stop now + $groups = $this->getGroups($meta, $config, $object); + foreach (array_keys($groups) as $group) { + if (array_key_exists($group, $changeSet)) { + continue 2; + } + } + + $oid = spl_object_id($object); + $pos = $meta->getFieldValue($object, $config['position']); + $matches = $pos >= $delta['start']; + $matches = $matches && ($delta['stop'] <= 0 || $pos < $delta['stop']); + $value = reset($relocation['groups']); + while ($matches && ($group = key($relocation['groups']))) { + $gr = $meta->getFieldValue($object, $group); + if (null === $value) { + $matches = null === $gr; + } elseif (is_object($gr) && is_object($value) && $gr !== $value) { + // Special case for equal objects but different instances. + // If the object implements Comparable interface we can use its compareTo method + // Otherwise we fallback to normal object comparison + if ($gr instanceof Comparable) { + $matches = $gr->compareTo($value); + // @todo: Remove "is_int" check and only support integer as the interface expects. + if (is_int($matches)) { + $matches = 0 === $matches; + } else { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2542', + 'Support for "%s" as return type from "%s::compareTo()" is deprecated since' + .' gedmo/doctrine-extensions 3.11 and will be removed in version 4.0. Return "integer" instead.', + gettype($matches), + Comparable::class + ); + } + } else { + $matches = $gr == $value; + } + } else { + $matches = $gr === $value; + } + $value = next($relocation['groups']); + } + if ($matches) { + // We cannot use `$this->setFieldValue()` here, because it will create a change set, that will + // prevent from other relocations being executed on this object. + // We just update the object value and will create the change set later. + if (!isset($updatedObjects[$oid])) { + $updatedObjects[$oid] = [ + 'object' => $object, + 'field' => $config['position'], + 'oldValue' => $pos, + ]; + } + $updatedObjects[$oid]['newValue'] = $pos + $delta['delta']; + + $meta->setFieldValue($object, $config['position'], $updatedObjects[$oid]['newValue']); + } + } + } + } + + foreach ($updatedObjects as $updateData) { + $this->setFieldValue($ea, $updateData['object'], $updateData['field'], $updateData['oldValue'], $updateData['newValue']); + } + + // Clear relocations + // unset only if relocations has been processed + unset($this->relocations[$hash], $this->maxPositions[$hash]); + } + } + + /** + * Computes node positions and updates the sort field in memory and in the db + * + * @param array $config + * @param ClassMetadata $meta + * @param object $object + * + * @phpstan-param SortableConfiguration $config + * + * @return void + */ + protected function processInsert(SortableAdapter $ea, array $config, $meta, $object) + { + $old = $meta->getFieldValue($object, $config['position']); + $newPosition = $meta->getFieldValue($object, $config['position']); + + if (null === $newPosition) { $newPosition = -1; } @@ -186,12 +382,12 @@ private function processInsert(SortableAdapter $ea, array $config, $meta, $objec } // Set position to max position if it is too big - $newPosition = min(array($this->maxPositions[$hash] + 1, $newPosition)); + $newPosition = min([$this->maxPositions[$hash] + 1, $newPosition]); // Compute relocations // New inserted entities should not be relocated by position update, so we exclude it. // Otherwise they could be relocated unintentionally. - $relocation = array($hash, $config['useObjectClass'], $groups, $newPosition, -1, +1, array($object)); + $relocation = [$hash, $config['useObjectClass'], $groups, $newPosition, -1, +1, [$object]]; // Apply existing relocations $applyDelta = 0; @@ -206,24 +402,26 @@ private function processInsert(SortableAdapter $ea, array $config, $meta, $objec $newPosition += $applyDelta; // Add relocations - call_user_func_array(array($this, 'addRelocation'), $relocation); + call_user_func_array([$this, 'addRelocation'], $relocation); // Set new position - if ($old < 0 || is_null($old)) { - $meta->getReflectionProperty($config['position'])->setValue($object, $newPosition); - $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); + if ($old < 0 || null === $old) { + $this->setFieldValue($ea, $object, $config['position'], $old, $newPosition); } } /** * Computes node positions and updates the sort field in memory and in the db * - * @param SortableAdapter $ea - * @param array $config - * @param ClassMetadata $meta - * @param object $object + * @param array $config + * @param ClassMetadata $meta + * @param object $object + * + * @phpstan-param SortableConfiguration $config + * + * @return void */ - private function processUpdate(SortableAdapter $ea, array $config, $meta, $object) + protected function processUpdate(SortableAdapter $ea, array $config, $meta, $object) { $em = $ea->getObjectManager(); $uow = $em->getUnitOfWork(); @@ -244,13 +442,16 @@ private function processUpdate(SortableAdapter $ea, array $config, $meta, $objec } } + $oldPosition = 0; + $newPosition = 0; + if ($changed) { $oldHash = $this->getHash($oldGroups, $config); $this->maxPositions[$oldHash] = $this->getMaxPosition($ea, $meta, $config, $object, $oldGroups); if (array_key_exists($config['position'], $changeSet)) { $oldPosition = $changeSet[$config['position']][0]; } else { - $oldPosition = $meta->getReflectionProperty($config['position'])->getValue($object); + $oldPosition = $meta->getFieldValue($object, $config['position']); } $this->addRelocation($oldHash, $config['useObjectClass'], $oldGroups, $oldPosition + 1, $this->maxPositions[$oldHash] + 1, -1); $groupHasChanged = true; @@ -290,10 +491,10 @@ private function processUpdate(SortableAdapter $ea, array $config, $meta, $objec // Compute position if it is negative if ($newPosition < 0) { - if ($oldPosition === -1) { - $newPosition += $this->maxPositions[$hash] + 2; // position == -1 => append at end of list + if (-1 === $oldPosition) { + $newPosition += $this->maxPositions[$hash] + 2; // position == -1 => append at end of list } else { - $newPosition += $this->maxPositions[$hash] + 1; // position == -1 => append at end of list + $newPosition += $this->maxPositions[$hash] + 1; // position == -1 => append at end of list } if ($newPosition < 0) { @@ -306,7 +507,7 @@ private function processUpdate(SortableAdapter $ea, array $config, $meta, $objec $newPosition = $this->maxPositions[$hash]; } } else { - $newPosition = min(array($this->maxPositions[$hash], $newPosition)); + $newPosition = min([$this->maxPositions[$hash], $newPosition]); } // Compute relocations @@ -325,48 +526,38 @@ private function processUpdate(SortableAdapter $ea, array $config, $meta, $objec |--node1--|--node3--|--node4--|--node2--|--node5--| */ $relocation = null; - if ($oldPosition === -1) { + if (-1 === $oldPosition) { // special case when group changes - $relocation = array($hash, $config['useObjectClass'], $groups, $newPosition, -1, +1); + $relocation = [$hash, $config['useObjectClass'], $groups, $newPosition, -1, +1]; } elseif ($newPosition < $oldPosition) { - $relocation = array($hash, $config['useObjectClass'], $groups, $newPosition, $oldPosition, +1); + $relocation = [$hash, $config['useObjectClass'], $groups, $newPosition, $oldPosition, +1]; } elseif ($newPosition > $oldPosition) { - $relocation = array($hash, $config['useObjectClass'], $groups, $oldPosition + 1, $newPosition + 1, -1); + $relocation = [$hash, $config['useObjectClass'], $groups, $oldPosition + 1, $newPosition + 1, -1]; } - // Apply existing relocations - $applyDelta = 0; - if (isset($this->relocations[$hash])) { - foreach ($this->relocations[$hash]['deltas'] as $delta) { - if ($delta['start'] <= $newPosition - && ($delta['stop'] > $newPosition || $delta['stop'] < 0)) { - $applyDelta += $delta['delta']; - } - } - } - $newPosition += $applyDelta; - if ($relocation) { // Add relocation - call_user_func_array(array($this, 'addRelocation'), $relocation); + call_user_func_array([$this, 'addRelocation'], $relocation); } // Set new position - $meta->getReflectionProperty($config['position'])->setValue($object, $newPosition); - $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); + $this->setFieldValue($ea, $object, $config['position'], $oldPosition, $newPosition); } /** * Computes node positions and updates the sort field in memory and in the db * - * @param SortableAdapter $ea - * @param array $config - * @param ClassMetadata $meta - * @param object $object + * @param array $config + * @param ClassMetadata $meta + * @param object $object + * + * @phpstan-param SortableConfiguration $config + * + * @return void */ - private function processDeletion(SortableAdapter $ea, array $config, $meta, $object) + protected function processDeletion(SortableAdapter $ea, array $config, $meta, $object) { - $position = $meta->getReflectionProperty($config['position'])->getValue($object); + $position = $meta->getFieldValue($object, $config['position']); // Get groups $groups = $this->getGroups($meta, $config, $object); @@ -385,9 +576,10 @@ private function processDeletion(SortableAdapter $ea, array $config, $meta, $obj /** * Persists relocations to database. - * @param SortableAdapter $ea + * + * @return void */ - private function persistRelocations(SortableAdapter $ea) + protected function persistRelocations(SortableAdapter $ea) { if (!$this->persistenceNeeded) { return; @@ -397,7 +589,7 @@ private function persistRelocations(SortableAdapter $ea) foreach ($this->relocations as $hash => $relocation) { $config = $this->getConfiguration($em, $relocation['name']); foreach ($relocation['deltas'] as $delta) { - if ($delta['start'] > $this->maxPositions[$hash] || $delta['delta'] == 0) { + if ($delta['start'] > $this->maxPositions[$hash] || 0 == $delta['delta']) { continue; } $ea->updatePositions($relocation, $delta, $config); @@ -408,87 +600,21 @@ private function persistRelocations(SortableAdapter $ea) } /** - * Sync objects in memory + * @param array $groups + * @param array $config + * + * @phpstan-param SortableConfiguration $config + * + * @return string */ - public function postFlush(EventArgs $args) - { - $ea = $this->getEventAdapter($args); - $em = $ea->getObjectManager(); - foreach ($this->relocations as $hash => $relocation) { - $config = $this->getConfiguration($em, $relocation['name']); - foreach ($relocation['deltas'] as $delta) { - if ($delta['start'] > $this->maxPositions[$hash] || $delta['delta'] == 0) { - continue; - } - - $meta = $em->getClassMetadata($relocation['name']); - - // now walk through the unit of work in memory objects and sync those - $uow = $em->getUnitOfWork(); - foreach ($uow->getIdentityMap() as $className => $objects) { - // for inheritance mapped classes, only root is always in the identity map - if ($className !== $ea->getRootObjectClass($meta) || !$this->getConfiguration($em, $className)) { - continue; - } - foreach ($objects as $object) { - if ($object instanceof Proxy && !$object->__isInitialized__) { - continue; - } - - $changeSet = $ea->getObjectChangeSet($uow, $object); - - // if the entity's position is already changed, stop now - if (array_key_exists($config['position'], $changeSet)) { - continue; - } - - // if the entity's group has changed, we stop now - $groups = $this->getGroups($meta, $config, $object); - foreach (array_keys($groups) as $group) { - if (array_key_exists($group, $changeSet)) { - continue 2; - } - } - - $oid = spl_object_hash($object); - $pos = $meta->getReflectionProperty($config['position'])->getValue($object); - $matches = $pos >= $delta['start']; - $matches = $matches && ($delta['stop'] <= 0 || $pos < $delta['stop']); - $value = reset($relocation['groups']); - while ($matches && ($group = key($relocation['groups']))) { - $gr = $meta->getReflectionProperty($group)->getValue($object); - if (null === $value) { - $matches = $gr === null; - } elseif (is_object($gr) && is_object($value) && $gr !== $value) { - // Special case for equal objects but different instances. - $matches = $gr == $value; - } else { - $matches = $gr === $value; - } - $value = next($relocation['groups']); - } - if ($matches) { - $meta->getReflectionProperty($config['position'])->setValue($object, $pos + $delta['delta']); - $ea->setOriginalObjectProperty($uow, $oid, $config['position'], $pos + $delta['delta']); - } - } - } - } - - // Clear relocations - unset($this->relocations[$hash]); - unset($this->maxPositions[$hash]); // unset only if relocations has been processed - } - } - - private function getHash($groups, array $config) + protected function getHash($groups, array $config) { $data = $config['useObjectClass']; foreach ($groups as $group => $val) { if ($val instanceof \DateTime) { $val = $val->format('c'); } elseif (is_object($val)) { - $val = spl_object_hash($val); + $val = spl_object_id($val); } $data .= $group.$val; } @@ -496,14 +622,24 @@ private function getHash($groups, array $config) return md5($data); } - private function getMaxPosition(SortableAdapter $ea, $meta, $config, $object, array $groups = array()) + /** + * @param ClassMetadata $meta + * @param array $config + * @param object $object + * @param array $groups + * + * @phpstan-param SortableConfiguration $config + * + * @return int + */ + protected function getMaxPosition(SortableAdapter $ea, $meta, $config, $object, array $groups = []) { $em = $ea->getObjectManager(); $uow = $em->getUnitOfWork(); $maxPos = null; // Get groups - if (!sizeof($groups)) { + if ([] === $groups) { $groups = $this->getGroups($meta, $config, $object); } @@ -525,37 +661,48 @@ private function getMaxPosition(SortableAdapter $ea, $meta, $config, $object, ar } $maxPos = $ea->getMaxPosition($config, $meta, $groups); - if (is_null($maxPos)) { + if (null === $maxPos) { $maxPos = -1; } - return intval($maxPos); + return (int) $maxPos; } /** * Add a relocation rule * - * @param string $hash The hash of the sorting group - * @param string $class The object class - * @param array $groups The sorting groups - * @param int $start Inclusive index to start relocation from - * @param int $stop Exclusive index to stop relocation at - * @param int $delta The delta to add to relocated nodes - * @param array $exclude Objects to be excluded from relocation + * @param string $hash The hash of the sorting group + * @param string $class The object class + * @param array $groups The sorting groups + * @param int $start Inclusive index to start relocation from + * @param int $stop Exclusive index to stop relocation at + * @param int $delta The delta to add to relocated nodes + * @param array $exclude Objects to be excluded from relocation + * + * @phpstan-param class-string $class + * + * @return void */ - private function addRelocation($hash, $class, $groups, $start, $stop, $delta, array $exclude = array()) + protected function addRelocation($hash, $class, $groups, $start, $stop, $delta, array $exclude = []) { if (!array_key_exists($hash, $this->relocations)) { - $this->relocations[$hash] = array('name' => $class, 'groups' => $groups, 'deltas' => array()); + $this->relocations[$hash] = ['name' => $class, 'groups' => $groups, 'deltas' => []]; } try { - $newDelta = array('start' => $start, 'stop' => $stop, 'delta' => $delta, 'exclude' => $exclude); - array_walk($this->relocations[$hash]['deltas'], function (&$val, $idx, $needle) { + $newDelta = ['start' => $start, 'stop' => $stop, 'delta' => $delta, 'exclude' => $exclude]; + array_walk($this->relocations[$hash]['deltas'], static function (&$val, $idx, $needle) { if ($val['start'] == $needle['start'] && $val['stop'] == $needle['stop']) { $val['delta'] += $needle['delta']; $val['exclude'] = array_merge($val['exclude'], $needle['exclude']); - throw new \Exception("Found delta. No need to add it again."); + + throw new \Exception('Found delta. No need to add it again.'); + } + + // For every deletion relocation add newly created object to the list of excludes + // otherwise position update queries will run for created objects as well. + if (-1 == $val['delta'] && 1 == $needle['delta']) { + $val['exclude'] = array_merge($val['exclude'], $needle['exclude']); } }, $newDelta); $this->relocations[$hash]['deltas'][] = $newDelta; @@ -564,28 +711,26 @@ private function addRelocation($hash, $class, $groups, $start, $stop, $delta, ar } /** + * @param ClassMetadata $meta + * @param array> $config + * @param object $object * - * @param array $config - * @param ClassMetadata $meta - * @param object $object + * @phpstan-param SortableConfiguration $config * - * @return array + * @return array */ - private function getGroups($meta, $config, $object) + protected function getGroups($meta, $config, $object) { - $groups = array(); + $groups = []; if (isset($config['groups'])) { foreach ($config['groups'] as $group) { - $groups[$group] = $meta->getReflectionProperty($group)->getValue($object); + $groups[$group] = $meta->getFieldValue($object, $group); } } return $groups; } - /** - * {@inheritDoc} - */ protected function getNamespace() { return __NAMESPACE__; diff --git a/src/Timestampable/Mapping/Driver/Annotation.php b/src/Timestampable/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..52d4d4bc18 --- /dev/null +++ b/src/Timestampable/Mapping/Driver/Annotation.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Timestampable\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the timestampable extension which reads extended metadata from annotations on a timestampable class. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/lib/Gedmo/Timestampable/Mapping/Driver/Annotation.php b/src/Timestampable/Mapping/Driver/Attribute.php similarity index 60% rename from lib/Gedmo/Timestampable/Mapping/Driver/Annotation.php rename to src/Timestampable/Mapping/Driver/Attribute.php index 418594c668..cf4c1c099e 100644 --- a/lib/Gedmo/Timestampable/Mapping/Driver/Annotation.php +++ b/src/Timestampable/Mapping/Driver/Attribute.php @@ -1,83 +1,105 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Timestampable\Mapping\Driver; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Timestampable; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an annotation mapping driver for Timestampable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Timestampable - * extension. + * Mapping driver for the timestampable extension which reads extended metadata from attributes on a timestampable class. * * @author Gediminas Morkevicius + * @author Kevin Mian Kraiker + * * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ -class Annotation extends AbstractAnnotationDriver +class Attribute extends AbstractAnnotationDriver { /** - * Annotation field is timestampable + * Mapping object for the timestampable extension. */ - const TIMESTAMPABLE = 'Gedmo\\Mapping\\Annotation\\Timestampable'; + public const TIMESTAMPABLE = Timestampable::class; /** * List of types which are valid for timestamp * - * @var array + * @var string[] */ - protected $validTypes = array( + protected $validTypes = [ 'date', + 'date_immutable', 'time', + 'time_immutable', 'datetime', + 'datetime_immutable', 'datetimetz', + 'datetimetz_immutable', 'timestamp', - 'zenddate', 'vardatetime', 'integer', - ); + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { $class = $this->getMetaReflectionClass($meta); + // property annotations foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() || - $meta->isInheritedField($property->name) || - isset($meta->associationMappings[$property->name]['inherited']) + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) ) { continue; } + if ($timestampable = $this->reader->getPropertyAnnotation($property, self::TIMESTAMPABLE)) { + \assert($timestampable instanceof Timestampable); + $field = $property->getName(); + if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find timestampable [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find timestampable [{$field}] as mapped property in entity - {$meta->getName()}"); } + if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'date', 'datetime' or 'time' in class - {$meta->name}"); + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'date', 'datetime' or 'time' in class - {$meta->getName()}"); } - if (!in_array($timestampable->on, array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + + if (!in_array($timestampable->on, ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($timestampable->on == 'change') { + + if ('change' === $timestampable->on) { if (!isset($timestampable->field)) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } + if (is_array($timestampable->field) && isset($timestampable->value)) { - throw new InvalidMappingException("Timestampable extension does not support multiple value changeset detection yet."); + throw new InvalidMappingException('Timestampable extension does not support multiple value changeset detection yet.'); } - $field = array( + + $field = [ 'field' => $field, 'trackedField' => $timestampable->field, 'value' => $timestampable->value, - ); + ]; } + // properties are unique and mapper checks that, no risk here $config[$timestampable->on][] = $field; } } + + return $config; } } diff --git a/lib/Gedmo/Timestampable/Mapping/Driver/Xml.php b/src/Timestampable/Mapping/Driver/Xml.php similarity index 65% rename from lib/Gedmo/Timestampable/Mapping/Driver/Xml.php rename to src/Timestampable/Mapping/Driver/Xml.php index b63c9fc2c0..0f15e876bd 100644 --- a/lib/Gedmo/Timestampable/Mapping/Driver/Xml.php +++ b/src/Timestampable/Mapping/Driver/Xml.php @@ -1,9 +1,17 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Timestampable\Mapping\Driver; -use Gedmo\Mapping\Driver\Xml as BaseXml; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for Timestampable @@ -13,90 +21,90 @@ * * @author Gediminas Morkevicius * @author Miha Vrhovnik - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ class Xml extends BaseXml { /** * List of types which are valid for timestamp * - * @var array + * @var string[] */ - private $validTypes = array( + private const VALID_TYPES = [ 'date', + 'date_immutable', 'time', + 'time_immutable', 'datetime', + 'datetime_immutable', 'datetimetz', + 'datetimetz_immutable', 'timestamp', - 'zenddate', 'vardatetime', 'integer', - ); + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { /** - * @var \SimpleXmlElement $mapping + * @var \SimpleXmlElement */ - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping->field)) { /** - * @var \SimpleXmlElement $fieldMapping + * @var \SimpleXmlElement */ foreach ($mapping->field as $fieldMapping) { $fieldMappingDoctrine = $fieldMapping; $fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($fieldMapping->timestampable)) { /** - * @var \SimpleXmlElement $data + * @var \SimpleXmlElement */ $data = $fieldMapping->timestampable; $field = $this->_getAttribute($fieldMappingDoctrine, 'name'); if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'date', 'datetime' or 'time' in class - {$meta->name}"); + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'date', 'datetime' or 'time' in class - {$meta->getName()}"); } - if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($this->_getAttribute($data, 'on') == 'change') { + if ('change' === $this->_getAttribute($data, 'on')) { if (!$this->_isAttributeSet($data, 'field')) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $this->_getAttribute($data, 'field'); - $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value' ) : null; - if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { - throw new InvalidMappingException("Timestampable extension does not support multiple value changeset detection yet."); - } - $field = array( + $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value') : null; + $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, - ); + ]; } $config[$this->_getAttribute($data, 'on')][] = $field; } } } + + return $config; } /** * Checks if $field type is valid * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validTypes); + return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true); } } diff --git a/lib/Gedmo/Timestampable/Mapping/Driver/Yaml.php b/src/Timestampable/Mapping/Driver/Yaml.php similarity index 65% rename from lib/Gedmo/Timestampable/Mapping/Driver/Yaml.php rename to src/Timestampable/Mapping/Driver/Yaml.php index 8bf2f8280b..5b9747a0fe 100644 --- a/lib/Gedmo/Timestampable/Mapping/Driver/Yaml.php +++ b/src/Timestampable/Mapping/Driver/Yaml.php @@ -1,10 +1,18 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Timestampable\Mapping\Driver; -use Gedmo\Mapping\Driver\File; -use Gedmo\Mapping\Driver; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Timestampable @@ -13,74 +21,77 @@ * extension. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ class Yaml extends File implements Driver { - /** - * File extension - * @var string - */ - protected $_extension = '.dcm.yml'; - /** * List of types which are valid for timestamp * - * @var array + * @var string[] */ - private $validTypes = array( + private const VALID_TYPES = [ 'date', + 'date_immutable', 'time', + 'time_immutable', 'datetime', + 'datetime_immutable', 'datetimetz', + 'datetimetz_immutable', 'timestamp', - 'zenddate', 'vardatetime', 'integer', - ); + ]; /** - * {@inheritDoc} + * File extension + * + * @var string */ + protected $_extension = '.dcm.yml'; + public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo']['timestampable'])) { $mappingProperty = $fieldMapping['gedmo']['timestampable']; if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'date', 'datetime' or 'time' in class - {$meta->name}"); + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'date', 'datetime' or 'time' in class - {$meta->getName()}"); } - if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], array('update', 'create', 'change'))) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}"); + if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } - if ($mappingProperty['on'] == 'change') { + if ('change' === $mappingProperty['on']) { if (!isset($mappingProperty['field'])) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}"); + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $mappingProperty['field']; - $valueAttribute = isset($mappingProperty['value']) ? $mappingProperty['value'] : null; + $valueAttribute = $mappingProperty['value'] ?? null; if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { - throw new InvalidMappingException("Timestampable extension does not support multiple value changeset detection yet."); + throw new InvalidMappingException('Timestampable extension does not support multiple value changeset detection yet.'); } - $field = array( + $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, - ); + ]; } $config[$mappingProperty['on']][] = $field; } } } + + return $config; } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); @@ -89,15 +100,15 @@ protected function _loadMappingFile($file) /** * Checks if $field type is valid * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validTypes); + return $mapping && in_array($mapping->type ?? $mapping['type'], self::VALID_TYPES, true); } } diff --git a/src/Timestampable/Mapping/Event/Adapter/ODM.php b/src/Timestampable/Mapping/Event/Adapter/ODM.php new file mode 100644 index 0000000000..9dc685d61e --- /dev/null +++ b/src/Timestampable/Mapping/Event/Adapter/ODM.php @@ -0,0 +1,52 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Timestampable\Mapping\Event\Adapter; + +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; +use Gedmo\Mapping\Event\ClockAwareAdapterInterface; +use Gedmo\Timestampable\Mapping\Event\TimestampableAdapter; +use Psr\Clock\ClockInterface; + +/** + * Doctrine event adapter for ODM adapted + * for Timestampable behavior + * + * @author Gediminas Morkevicius + */ +final class ODM extends BaseAdapterODM implements TimestampableAdapter, ClockAwareAdapterInterface +{ + private ?ClockInterface $clock = null; + + public function setClock(ClockInterface $clock): void + { + $this->clock = $clock; + } + + /** + * @param ClassMetadata $meta + */ + public function getDateValue($meta, $field) + { + $datetime = $this->clock instanceof ClockInterface ? $this->clock->now() : new \DateTimeImmutable(); + $mapping = $meta->getFieldMapping($field); + $type = $mapping['type'] ?? null; + + if ('timestamp' === $type) { + return (int) $datetime->format('U'); + } + + if (in_array($type, ['date_immutable', 'time_immutable', 'datetime_immutable', 'datetimetz_immutable'], true)) { + return $datetime; + } + + return \DateTime::createFromImmutable($datetime); + } +} diff --git a/src/Timestampable/Mapping/Event/Adapter/ORM.php b/src/Timestampable/Mapping/Event/Adapter/ORM.php new file mode 100644 index 0000000000..276bc05c02 --- /dev/null +++ b/src/Timestampable/Mapping/Event/Adapter/ORM.php @@ -0,0 +1,70 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Timestampable\Mapping\Event\Adapter; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\FieldMapping; +use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; +use Gedmo\Mapping\Event\ClockAwareAdapterInterface; +use Gedmo\Timestampable\Mapping\Event\TimestampableAdapter; +use Psr\Clock\ClockInterface; + +/** + * Doctrine event adapter for ORM adapted + * for Timestampable behavior + * + * @author Gediminas Morkevicius + */ +final class ORM extends BaseAdapterORM implements TimestampableAdapter, ClockAwareAdapterInterface +{ + private ?ClockInterface $clock = null; + + public function setClock(ClockInterface $clock): void + { + $this->clock = $clock; + } + + /** + * @param ClassMetadata $meta + */ + public function getDateValue($meta, $field) + { + $mapping = $meta->getFieldMapping($field); + + return $this->getObjectManager()->getConnection()->convertToPHPValue( + $this->getRawDateValue($mapping), + $mapping instanceof FieldMapping ? $mapping->type : ($mapping['type'] ?? Types::DATETIME_MUTABLE) + ); + } + + /** + * Generates current timestamp for the specified mapping + * + * @param array|FieldMapping $mapping + * + * @return \DateTimeInterface|int + */ + private function getRawDateValue($mapping) + { + $datetime = $this->clock instanceof ClockInterface ? $this->clock->now() : new \DateTimeImmutable(); + $type = $mapping instanceof FieldMapping ? $mapping->type : ($mapping['type'] ?? ''); + + if ('integer' === $type) { + return (int) $datetime->format('U'); + } + + if (in_array($type, ['date_immutable', 'time_immutable', 'datetime_immutable', 'datetimetz_immutable'], true)) { + return $datetime; + } + + return \DateTime::createFromImmutable($datetime); + } +} diff --git a/src/Timestampable/Mapping/Event/TimestampableAdapter.php b/src/Timestampable/Mapping/Event/TimestampableAdapter.php new file mode 100644 index 0000000000..c6e61458d3 --- /dev/null +++ b/src/Timestampable/Mapping/Event/TimestampableAdapter.php @@ -0,0 +1,31 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Timestampable\Mapping\Event; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Mapping\Event\AdapterInterface; + +/** + * Doctrine event adapter for the Timestampable extension. + * + * @author Gediminas Morkevicius + */ +interface TimestampableAdapter extends AdapterInterface +{ + /** + * Get the date value. + * + * @param ClassMetadata $meta + * @param string $field + * + * @return int|\DateTimeInterface + */ + public function getDateValue($meta, $field); +} diff --git a/src/Timestampable/Timestampable.php b/src/Timestampable/Timestampable.php new file mode 100644 index 0000000000..2cc441a89f --- /dev/null +++ b/src/Timestampable/Timestampable.php @@ -0,0 +1,54 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Timestampable; + +/** + * Marker interface for objects which can be identified as timestampable. + * + * @author Gediminas Morkevicius + */ +interface Timestampable +{ + // timestampable expects annotations on properties + + /* + * @Gedmo\Timestampable(on="create") + * dates which should be updated on insert only + */ + + /* + * @Gedmo\Timestampable(on="update") + * dates which should be updated on update and insert + */ + + /* + * @Gedmo\Timestampable(on="change", field="field", value="value") + * dates which should be updated on changed "property" + * value and become equal to given "value" + */ + + /* + * @Gedmo\Timestampable(on="change", field="field") + * dates which should be updated on changed "property" + */ + + /* + * @Gedmo\Timestampable(on="change", fields={"field1", "field2"}) + * dates which should be updated if at least one of the given fields changed + */ + + /* + * example + * + * @Gedmo\Timestampable(on="create") + * @Column(type="date") + * $created + */ +} diff --git a/lib/Gedmo/Timestampable/TimestampableListener.php b/src/Timestampable/TimestampableListener.php similarity index 50% rename from lib/Gedmo/Timestampable/TimestampableListener.php rename to src/Timestampable/TimestampableListener.php index 59d8469721..8b6dfeb78c 100644 --- a/lib/Gedmo/Timestampable/TimestampableListener.php +++ b/src/Timestampable/TimestampableListener.php @@ -1,8 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Timestampable; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\AbstractTrackingListener; use Gedmo\Timestampable\Mapping\Event\TimestampableAdapter; @@ -11,14 +18,18 @@ * dates on creation and update. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @phpstan-extends AbstractTrackingListener + * + * @final since gedmo/doctrine-extensions 3.11 */ class TimestampableListener extends AbstractTrackingListener { /** - * @param ClassMetadata $meta - * @param string $field - * @param TimestampableAdapter $eventAdapter + * @param ClassMetadata $meta + * @param string $field + * @param TimestampableAdapter $eventAdapter + * * @return mixed */ protected function getFieldValue($meta, $field, $eventAdapter) @@ -26,9 +37,6 @@ protected function getFieldValue($meta, $field, $eventAdapter) return $eventAdapter->getDateValue($meta, $field); } - /** - * {@inheritDoc} - */ protected function getNamespace() { return __NAMESPACE__; diff --git a/lib/Gedmo/Timestampable/Traits/Timestampable.php b/src/Timestampable/Traits/Timestampable.php similarity index 63% rename from lib/Gedmo/Timestampable/Traits/Timestampable.php rename to src/Timestampable/Traits/Timestampable.php index f15afbca5b..8ee48c85df 100644 --- a/lib/Gedmo/Timestampable/Traits/Timestampable.php +++ b/src/Timestampable/Traits/Timestampable.php @@ -1,29 +1,36 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Timestampable\Traits; /** - * Timestampable Trait, usable with PHP >= 5.4 + * Trait for timestampable objects. + * + * This implementation does not provide any mapping configurations. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ trait Timestampable { /** - * @var \DateTime + * @var \DateTime|null */ protected $createdAt; /** - * @var \DateTime + * @var \DateTime|null */ protected $updatedAt; /** * Sets createdAt. * - * @param \DateTime $createdAt * @return $this */ public function setCreatedAt(\DateTime $createdAt) @@ -36,7 +43,7 @@ public function setCreatedAt(\DateTime $createdAt) /** * Returns createdAt. * - * @return \DateTime + * @return \DateTime|null */ public function getCreatedAt() { @@ -46,7 +53,6 @@ public function getCreatedAt() /** * Sets updatedAt. * - * @param \DateTime $updatedAt * @return $this */ public function setUpdatedAt(\DateTime $updatedAt) @@ -59,7 +65,7 @@ public function setUpdatedAt(\DateTime $updatedAt) /** * Returns updatedAt. * - * @return \DateTime + * @return \DateTime|null */ public function getUpdatedAt() { diff --git a/lib/Gedmo/Timestampable/Traits/TimestampableDocument.php b/src/Timestampable/Traits/TimestampableDocument.php similarity index 56% rename from lib/Gedmo/Timestampable/Traits/TimestampableDocument.php rename to src/Timestampable/Traits/TimestampableDocument.php index c133689846..31a6ce572e 100644 --- a/lib/Gedmo/Timestampable/Traits/TimestampableDocument.php +++ b/src/Timestampable/Traits/TimestampableDocument.php @@ -1,36 +1,52 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Timestampable\Traits; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** - * Timestampable Trait, usable with PHP >= 5.4 + * Trait for timestampable objects. + * + * This implementation provides a mapping configuration for the Doctrine MongoDB ODM. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ trait TimestampableDocument { /** - * @var \DateTime + * @var \DateTime|null + * * @Gedmo\Timestampable(on="create") - * @ODM\Date + * + * @ODM\Field(type="date") */ + #[Gedmo\Timestampable(on: 'create')] + #[ODM\Field(type: Type::DATE)] protected $createdAt; /** - * @var \DateTime + * @var \DateTime|null + * * @Gedmo\Timestampable(on="update") - * @ODM\Date + * + * @ODM\Field(type="date") */ + #[Gedmo\Timestampable(on: 'update')] + #[ODM\Field(type: Type::DATE)] protected $updatedAt; /** * Sets createdAt. * - * @param \Datetime $createdAt * @return $this */ public function setCreatedAt(\DateTime $createdAt) @@ -43,7 +59,7 @@ public function setCreatedAt(\DateTime $createdAt) /** * Returns createdAt. * - * @return \DateTime + * @return \DateTime|null */ public function getCreatedAt() { @@ -53,7 +69,6 @@ public function getCreatedAt() /** * Sets updatedAt. * - * @param \DateTime $updatedAt * @return $this */ public function setUpdatedAt(\DateTime $updatedAt) @@ -66,7 +81,7 @@ public function setUpdatedAt(\DateTime $updatedAt) /** * Returns updatedAt. * - * @return \Datetime + * @return \Datetime|null */ public function getUpdatedAt() { diff --git a/lib/Gedmo/Timestampable/Traits/TimestampableEntity.php b/src/Timestampable/Traits/TimestampableEntity.php similarity index 59% rename from lib/Gedmo/Timestampable/Traits/TimestampableEntity.php rename to src/Timestampable/Traits/TimestampableEntity.php index 9f638c9ac7..a224e67593 100644 --- a/lib/Gedmo/Timestampable/Traits/TimestampableEntity.php +++ b/src/Timestampable/Traits/TimestampableEntity.php @@ -1,36 +1,52 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Timestampable\Traits; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** - * Timestampable Trait, usable with PHP >= 5.4 + * Trait for timestampable objects. + * + * This implementation provides a mapping configuration for the Doctrine ORM. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ trait TimestampableEntity { /** - * @var \DateTime + * @var \DateTime|null + * * @Gedmo\Timestampable(on="create") + * * @ORM\Column(type="datetime") */ + #[Gedmo\Timestampable(on: 'create')] + #[ORM\Column(type: Types::DATETIME_MUTABLE)] protected $createdAt; /** - * @var \DateTime + * @var \DateTime|null + * * @Gedmo\Timestampable(on="update") + * * @ORM\Column(type="datetime") */ + #[Gedmo\Timestampable(on: 'update')] + #[ORM\Column(type: Types::DATETIME_MUTABLE)] protected $updatedAt; /** * Sets createdAt. * - * @param \DateTime $createdAt * @return $this */ public function setCreatedAt(\DateTime $createdAt) @@ -43,7 +59,7 @@ public function setCreatedAt(\DateTime $createdAt) /** * Returns createdAt. * - * @return \DateTime + * @return \DateTime|null */ public function getCreatedAt() { @@ -53,7 +69,6 @@ public function getCreatedAt() /** * Sets updatedAt. * - * @param \DateTime $updatedAt * @return $this */ public function setUpdatedAt(\DateTime $updatedAt) @@ -66,7 +81,7 @@ public function setUpdatedAt(\DateTime $updatedAt) /** * Returns updatedAt. * - * @return \DateTime + * @return \DateTime|null */ public function getUpdatedAt() { diff --git a/src/Tool/ActorProviderInterface.php b/src/Tool/ActorProviderInterface.php new file mode 100644 index 0000000000..3a8334d8de --- /dev/null +++ b/src/Tool/ActorProviderInterface.php @@ -0,0 +1,21 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool; + +/** + * Interface for a provider for an actor for extensions supporting actor/user references. + */ +interface ActorProviderInterface +{ + /** + * @return object|string|null + */ + public function getActor(); +} diff --git a/src/Tool/ClassUtils.php b/src/Tool/ClassUtils.php new file mode 100644 index 0000000000..5edbe321e0 --- /dev/null +++ b/src/Tool/ClassUtils.php @@ -0,0 +1,44 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool; + +use Doctrine\Common\Util\ClassUtils as CommonClassUtils; + +/** + * Utility class for Doctrine Common proxies. + */ +final class ClassUtils +{ + private function __construct() + { + } + + /** + * Gets the real class name of an object (even if it's a proxy). + * + * If doctrine/common is not installed, this method behaves like {@see get_class()}. + * + * @param TObject $object + * + * @return class-string + * + * @template TObject of object + */ + public static function getClass(object $object): string + { + if (class_exists(CommonClassUtils::class)) { + return CommonClassUtils::getClass($object); + } + + return get_class($object); + } +} diff --git a/src/Tool/IpAddressProviderInterface.php b/src/Tool/IpAddressProviderInterface.php new file mode 100644 index 0000000000..ceb0acdb3d --- /dev/null +++ b/src/Tool/IpAddressProviderInterface.php @@ -0,0 +1,18 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool; + +/** + * Interface for a provider for an IP address for extensions supporting IP references. + */ +interface IpAddressProviderInterface +{ + public function getAddress(): ?string; +} diff --git a/lib/Gedmo/Tool/Logging/DBAL/QueryAnalyzer.php b/src/Tool/Logging/DBAL/QueryAnalyzer.php similarity index 70% rename from lib/Gedmo/Tool/Logging/DBAL/QueryAnalyzer.php rename to src/Tool/Logging/DBAL/QueryAnalyzer.php index 40cd520978..e57c2b23ec 100644 --- a/lib/Gedmo/Tool/Logging/DBAL/QueryAnalyzer.php +++ b/src/Tool/Logging/DBAL/QueryAnalyzer.php @@ -1,14 +1,24 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tool\Logging\DBAL; use Doctrine\DBAL\Logging\SQLLogger; -use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type; /** * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5. + * + * @final since gedmo/doctrine-extensions 3.11 */ class QueryAnalyzer implements SQLLogger { @@ -21,39 +31,33 @@ class QueryAnalyzer implements SQLLogger /** * Start time of currently executed query - * - * @var integer */ - private $queryStartTime = null; + private ?float $queryStartTime = null; /** * Total execution time of all queries - * - * @var integer */ - private $totalExecutionTime = 0; + private int $totalExecutionTime = 0; /** * List of queries executed * - * @var array + * @var string[] */ - private $queries = array(); + private array $queries = []; /** * Query execution times indexed * in same order as queries * - * @var array + * @var float[] */ - private $queryExecutionTimes = array(); + private array $queryExecutionTimes = []; /** * Initialize log listener with database * platform, which is needed for parameter * conversion - * - * @param AbstractPlatform $platform */ public function __construct(AbstractPlatform $platform) { @@ -61,20 +65,20 @@ public function __construct(AbstractPlatform $platform) } /** - * {@inheritdoc} + * @return void */ - public function startQuery($sql, array $params = null, array $types = null) + public function startQuery($sql, ?array $params = null, ?array $types = null) { $this->queryStartTime = microtime(true); $this->queries[] = $this->generateSql($sql, $params, $types); } /** - * {@inheritdoc} + * @return void */ public function stopQuery() { - $ms = round(microtime(true) - $this->queryStartTime, 4) * 1000; + $ms = (int) (round(microtime(true) - $this->queryStartTime, 4) * 1000); $this->queryExecutionTimes[] = $ms; $this->totalExecutionTime += $ms; } @@ -86,8 +90,8 @@ public function stopQuery() */ public function cleanUp() { - $this->queries = array(); - $this->queryExecutionTimes = array(); + $this->queries = []; + $this->queryExecutionTimes = []; $this->totalExecutionTime = 0; return $this; @@ -96,7 +100,7 @@ public function cleanUp() /** * Dump the statistics of executed queries * - * @param boolean $dumpOnlySql + * @param bool $dumpOnlySql * * @return string */ @@ -109,7 +113,7 @@ public function getOutput($dumpOnlySql = false) } foreach ($this->queries as $index => $sql) { if (!$dumpOnlySql) { - $output .= 'Query('.($index+1).') - '.$this->queryExecutionTimes[$index].' ms'.PHP_EOL; + $output .= 'Query('.($index + 1).') - '.$this->queryExecutionTimes[$index].' ms'.PHP_EOL; } $output .= $sql.';'.PHP_EOL; } @@ -121,7 +125,7 @@ public function getOutput($dumpOnlySql = false) /** * Index of the slowest query executed * - * @return integer + * @return int */ public function getSlowestQueryIndex() { @@ -140,7 +144,7 @@ public function getSlowestQueryIndex() /** * Get total execution time of queries * - * @return integer + * @return float */ public function getTotalExecutionTime() { @@ -150,7 +154,7 @@ public function getTotalExecutionTime() /** * Get all queries * - * @return array + * @return string[] */ public function getExecutedQueries() { @@ -160,7 +164,7 @@ public function getExecutedQueries() /** * Get number of executed queries * - * @return integer + * @return int */ public function getNumExecutedQueries() { @@ -170,7 +174,7 @@ public function getNumExecutedQueries() /** * Get all query execution times * - * @return array + * @return float[] */ public function getExecutionTimes() { @@ -180,21 +184,18 @@ public function getExecutionTimes() /** * Create the SQL with mapped parameters * - * @param string $sql - * @param array $params - * @param array $types - * - * @return string + * @param array|null $params + * @param array|null $types */ - private function generateSql($sql, $params, $types) + private function generateSql(string $sql, ?array $params, ?array $types): string { - if (!count($params)) { + if (null === $params || [] === $params) { return $sql; } $converted = $this->getConvertedParams($params, $types); if (is_int(key($params))) { $index = key($converted); - $sql = preg_replace_callback('@\?@sm', function ($match) use (&$index, $converted) { + $sql = preg_replace_callback('@\?@sm', static function ($match) use (&$index, $converted) { return $converted[$index++]; }, $sql); } else { @@ -209,14 +210,14 @@ private function generateSql($sql, $params, $types) /** * Get the converted parameter list * - * @param array $params - * @param array $types + * @param array $params + * @param array $types * - * @return array + * @return array */ - private function getConvertedParams($params, $types) + private function getConvertedParams(array $params, array $types): array { - $result = array(); + $result = []; foreach ($params as $position => $value) { if (isset($types[$position])) { $type = $types[$position]; @@ -227,16 +228,16 @@ private function getConvertedParams($params, $types) $value = $type->convertToDatabaseValue($value, $this->platform); } } else { - if (is_object($value) && $value instanceof \DateTime) { + if ($value instanceof \DateTimeInterface) { $value = $value->format($this->platform->getDateTimeFormatString()); - } elseif (!is_null($value)) { + } elseif (null !== $value) { $type = Type::getType(gettype($value)); $value = $type->convertToDatabaseValue($value, $this->platform); } } if (is_string($value)) { $value = "'{$value}'"; - } elseif (is_null($value)) { + } elseif (null === $value) { $value = 'NULL'; } $result[$position] = $value; diff --git a/src/Tool/ORM/Hydration/EntityManagerRetriever.php b/src/Tool/ORM/Hydration/EntityManagerRetriever.php new file mode 100644 index 0000000000..d343f4a4ad --- /dev/null +++ b/src/Tool/ORM/Hydration/EntityManagerRetriever.php @@ -0,0 +1,31 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool\ORM\Hydration; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Internal\Hydration\AbstractHydrator; + +/** + * Helper method to retrieve the entity manager for ORM hydrator classes. + * + * This trait includes a compatibility layer for the renamed `Doctrine\ORM\Internal\Hydration\AbstractHydrator::$_em` + * property between ORM 2.x and 3.x. + * + * @mixin AbstractHydrator + * + * @internal + */ +trait EntityManagerRetriever +{ + protected function getEntityManager(): EntityManagerInterface + { + return property_exists($this, '_em') ? $this->_em : $this->em; + } +} diff --git a/src/Tool/ORM/Hydration/HydratorCompat.php b/src/Tool/ORM/Hydration/HydratorCompat.php new file mode 100644 index 0000000000..2a2ee7c7eb --- /dev/null +++ b/src/Tool/ORM/Hydration/HydratorCompat.php @@ -0,0 +1,131 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool\ORM\Hydration; + +use Doctrine\ORM\Internal\Hydration\AbstractHydrator; + +// The methods we need the compat bridge for are protected, so we're using a public method for this check +if ((new \ReflectionClass(AbstractHydrator::class))->getMethod('onClear')->hasReturnType()) { + // ORM 3.x + /** + * Helper trait to address compatibility issues between ORM 2.x and 3.x. + * + * @mixin AbstractHydrator + * + * @internal + */ + trait HydratorCompat + { + /** + * Executes one-time preparation tasks, once each time hydration is started + * through {@link hydrateAll} or {@link toIterable()}. + */ + protected function prepare(): void + { + $this->doPrepareWithCompat(); + } + + protected function doPrepareWithCompat(): void + { + parent::prepare(); + } + + /** + * Executes one-time cleanup tasks at the end of a hydration that was initiated + * through {@link hydrateAll} or {@link toIterable()}. + */ + protected function cleanup(): void + { + $this->doCleanupWithCompat(); + } + + protected function doCleanupWithCompat(): void + { + parent::cleanup(); + } + + /** + * Hydrates all rows from the current statement instance at once. + */ + protected function hydrateAllData(): array + { + return $this->doHydrateAllData(); + } + + /** + * @return mixed[] + */ + protected function doHydrateAllData() + { + return parent::hydrateAllData(); + } + } +} else { + // ORM 2.x + /** + * Helper trait to address compatibility issues between ORM 2.x and 3.x. + * + * @mixin AbstractHydrator + * + * @internal + */ + trait HydratorCompat + { + /** + * Executes one-time preparation tasks, once each time hydration is started + * through {@link hydrateAll} or {@link toIterable()}. + * + * @return void + */ + protected function prepare() + { + $this->doPrepareWithCompat(); + } + + protected function doPrepareWithCompat(): void + { + parent::prepare(); + } + + /** + * Executes one-time cleanup tasks at the end of a hydration that was initiated + * through {@link hydrateAll} or {@link toIterable()}. + * + * @return void + */ + protected function cleanup() + { + $this->doCleanupWithCompat(); + } + + protected function doCleanupWithCompat(): void + { + parent::cleanup(); + } + + /** + * Hydrates all rows from the current statement instance at once. + * + * @return mixed[] + */ + protected function hydrateAllData() + { + return $this->doHydrateAllData(); + } + + /** + * @return mixed[] + */ + protected function doHydrateAllData() + { + return parent::hydrateAllData(); + } + } +} diff --git a/src/Tool/ORM/Repository/EntityRepositoryCompat.php b/src/Tool/ORM/Repository/EntityRepositoryCompat.php new file mode 100644 index 0000000000..586dec7c4c --- /dev/null +++ b/src/Tool/ORM/Repository/EntityRepositoryCompat.php @@ -0,0 +1,77 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool\ORM\Repository; + +use Doctrine\ORM\EntityRepository; + +if ((new \ReflectionClass(EntityRepository::class))->getMethod('__call')->hasReturnType()) { + // ORM 3.x + /** + * Helper trait to address compatibility issues between ORM 2.x and 3.x. + * + * @mixin EntityRepository + * + * @internal + */ + trait EntityRepositoryCompat + { + /** + * @phpstan-param list $args + */ + public function __call(string $method, array $args): mixed + { + return $this->doCallWithCompat($method, $args); + } + + /** + * @param string $method + * @param array $args + * + * @phpstan-param list $args + * + * @return mixed + */ + abstract protected function doCallWithCompat($method, $args); + } +} else { + // ORM 2.x + /** + * Helper trait to address compatibility issues between ORM 2.x and 3.x. + * + * @mixin EntityRepository + * + * @internal + */ + trait EntityRepositoryCompat + { + /** + * @param string $method + * @param array $args + * + * @phpstan-param list $args + * + * @return mixed + */ + public function __call($method, $args) + { + return $this->doCallWithCompat($method, $args); + } + + /** + * @param string $method + * @param array $args + * + * @phpstan-param list $args + * + * @return mixed + */ + abstract protected function doCallWithCompat($method, $args); + } +} diff --git a/src/Tool/ORM/Walker/SqlWalkerCompat.php b/src/Tool/ORM/Walker/SqlWalkerCompat.php new file mode 100644 index 0000000000..24721d729a --- /dev/null +++ b/src/Tool/ORM/Walker/SqlWalkerCompat.php @@ -0,0 +1,38 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool\ORM\Walker; + +use Doctrine\ORM\Query\SqlWalker; + +if ((new \ReflectionClass(SqlWalker::class))->getMethod('getExecutor')->hasReturnType()) { + /** + * Helper trait to address compatibility issues between ORM 2.x and 3.x. + * + * @mixin SqlWalker + * + * @internal + */ + trait SqlWalkerCompat + { + use SqlWalkerCompatForOrm3; + } +} else { + /** + * Helper trait to address compatibility issues between ORM 2.x and 3.x. + * + * @mixin SqlWalker + * + * @internal + */ + trait SqlWalkerCompat + { + use SqlWalkerCompatForOrm2; + } +} diff --git a/src/Tool/ORM/Walker/SqlWalkerCompatForOrm2.php b/src/Tool/ORM/Walker/SqlWalkerCompatForOrm2.php new file mode 100644 index 0000000000..da24d2bf54 --- /dev/null +++ b/src/Tool/ORM/Walker/SqlWalkerCompatForOrm2.php @@ -0,0 +1,229 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool\ORM\Walker; + +use Doctrine\ORM\Query\AST\DeleteClause; +use Doctrine\ORM\Query\AST\DeleteStatement; +use Doctrine\ORM\Query\AST\FromClause; +use Doctrine\ORM\Query\AST\GroupByClause; +use Doctrine\ORM\Query\AST\HavingClause; +use Doctrine\ORM\Query\AST\OrderByClause; +use Doctrine\ORM\Query\AST\SelectClause; +use Doctrine\ORM\Query\AST\SelectStatement; +use Doctrine\ORM\Query\AST\SimpleSelectClause; +use Doctrine\ORM\Query\AST\SubselectFromClause; +use Doctrine\ORM\Query\AST\UpdateStatement; +use Doctrine\ORM\Query\AST\WhereClause; +use Doctrine\ORM\Query\Exec\AbstractSqlExecutor; +use Doctrine\ORM\Query\Exec\SqlFinalizer; +use Doctrine\ORM\Query\SqlWalker; + +/** + * Helper trait to address compatibility issues between ORM 2.x and 3.x. + * + * @mixin SqlWalker + * + * @internal + */ +trait SqlWalkerCompatForOrm2 +{ + /** + * Gets an executor that can be used to execute the result of this walker. + * + * @param SelectStatement|UpdateStatement|DeleteStatement $statement + * + * @return AbstractSqlExecutor + */ + public function getExecutor($statement) + { + return $this->doGetExecutorWithCompat($statement); + } + + /** + * @param DeleteStatement|UpdateStatement|SelectStatement $AST + */ + public function getFinalizer($AST): SqlFinalizer + { + return $this->doGetFinalizerWithCompat($AST); + } + + /** + * Walks down a SelectClause AST node, thereby generating the appropriate SQL. + * + * @param SelectClause $selectClause + * + * @return string + */ + public function walkSelectClause($selectClause) + { + return $this->doWalkSelectClauseWithCompat($selectClause); + } + + /** + * Walks down a FromClause AST node, thereby generating the appropriate SQL. + * + * @param FromClause $fromClause + * + * @return string + */ + public function walkFromClause($fromClause) + { + return $this->doWalkFromClauseWithCompat($fromClause); + } + + /** + * Walks down a OrderByClause AST node, thereby generating the appropriate SQL. + * + * @param OrderByClause $orderByClause + * + * @return string + */ + public function walkOrderByClause($orderByClause) + { + return $this->doWalkOrderByClauseWithCompat($orderByClause); + } + + /** + * Walks down a HavingClause AST node, thereby generating the appropriate SQL. + * + * @param HavingClause $havingClause + * + * @return string + */ + public function walkHavingClause($havingClause) + { + return $this->doWalkHavingClauseWithCompat($havingClause); + } + + /** + * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. + * + * @param SubselectFromClause $subselectFromClause + * + * @return string + */ + public function walkSubselectFromClause($subselectFromClause) + { + return $this->doWalkSubselectFromClauseWithCompat($subselectFromClause); + } + + /** + * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectClause $simpleSelectClause + * + * @return string + */ + public function walkSimpleSelectClause($simpleSelectClause) + { + return $this->doWalkSimpleSelectClauseWithCompat($simpleSelectClause); + } + + /** + * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. + * + * @param GroupByClause $groupByClause + * + * @return string + */ + public function walkGroupByClause($groupByClause) + { + return $this->doWalkGroupByClauseWithCompat($groupByClause); + } + + /** + * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. + * + * @param DeleteClause $deleteClause + * + * @return string + */ + public function walkDeleteClause($deleteClause) + { + return $this->doWalkDeleteClauseWithCompat($deleteClause); + } + + /** + * Walks down a WhereClause AST node, thereby generating the appropriate SQL. + * + * WhereClause or not, the appropriate discriminator sql is added. + * + * @param WhereClause|null $whereClause + * + * @return string + */ + public function walkWhereClause($whereClause) + { + return $this->doWalkWhereClauseWithCompat($whereClause); + } + + /** + * Gets an executor that can be used to execute the result of this walker. + * + * @param SelectStatement|UpdateStatement|DeleteStatement $statement + */ + protected function doGetExecutorWithCompat($statement): AbstractSqlExecutor + { + return parent::getExecutor($statement); + } + + /** + * @param DeleteStatement|UpdateStatement|SelectStatement $AST + */ + protected function doGetFinalizerWithCompat($AST): SqlFinalizer + { + return parent::getFinalizer($AST); + } + + protected function doWalkSelectClauseWithCompat(SelectClause $selectClause): string + { + return parent::walkSelectClause($selectClause); + } + + protected function doWalkFromClauseWithCompat(FromClause $fromClause): string + { + return parent::walkFromClause($fromClause); + } + + protected function doWalkOrderByClauseWithCompat(OrderByClause $orderByClause): string + { + return parent::walkOrderByClause($orderByClause); + } + + protected function doWalkHavingClauseWithCompat(HavingClause $havingClause): string + { + return parent::walkHavingClause($havingClause); + } + + protected function doWalkSubselectFromClauseWithCompat(SubselectFromClause $subselectFromClause): string + { + return parent::walkSubselectFromClause($subselectFromClause); + } + + protected function doWalkSimpleSelectClauseWithCompat(SimpleSelectClause $simpleSelectClause): string + { + return parent::walkSimpleSelectClause($simpleSelectClause); + } + + protected function doWalkGroupByClauseWithCompat(GroupByClause $groupByClause): string + { + return parent::walkGroupByClause($groupByClause); + } + + protected function doWalkDeleteClauseWithCompat(DeleteClause $deleteClause): string + { + return parent::walkDeleteClause($deleteClause); + } + + protected function doWalkWhereClauseWithCompat(?WhereClause $whereClause): string + { + return parent::walkWhereClause($whereClause); + } +} diff --git a/src/Tool/ORM/Walker/SqlWalkerCompatForOrm3.php b/src/Tool/ORM/Walker/SqlWalkerCompatForOrm3.php new file mode 100644 index 0000000000..cc33b706b8 --- /dev/null +++ b/src/Tool/ORM/Walker/SqlWalkerCompatForOrm3.php @@ -0,0 +1,186 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool\ORM\Walker; + +use Doctrine\ORM\Query\AST\DeleteClause; +use Doctrine\ORM\Query\AST\DeleteStatement; +use Doctrine\ORM\Query\AST\FromClause; +use Doctrine\ORM\Query\AST\GroupByClause; +use Doctrine\ORM\Query\AST\HavingClause; +use Doctrine\ORM\Query\AST\OrderByClause; +use Doctrine\ORM\Query\AST\SelectClause; +use Doctrine\ORM\Query\AST\SelectStatement; +use Doctrine\ORM\Query\AST\SimpleSelectClause; +use Doctrine\ORM\Query\AST\SubselectFromClause; +use Doctrine\ORM\Query\AST\UpdateStatement; +use Doctrine\ORM\Query\AST\WhereClause; +use Doctrine\ORM\Query\Exec\AbstractSqlExecutor; +use Doctrine\ORM\Query\Exec\SqlFinalizer; +use Doctrine\ORM\Query\SqlWalker; + +/** + * Helper trait to address compatibility issues between ORM 2.x and 3.x. + * + * @mixin SqlWalker + * + * @internal + */ +trait SqlWalkerCompatForOrm3 +{ + /** + * Gets an executor that can be used to execute the result of this walker. + */ + public function getExecutor(SelectStatement|UpdateStatement|DeleteStatement $statement): AbstractSqlExecutor + { + return $this->doGetExecutorWithCompat($statement); + } + + public function getFinalizer(DeleteStatement|UpdateStatement|SelectStatement $AST): SqlFinalizer + { + return $this->doGetFinalizerWithCompat($AST); + } + + /** + * Walks down a SelectClause AST node, thereby generating the appropriate SQL. + */ + public function walkSelectClause(SelectClause $selectClause): string + { + return $this->doWalkSelectClauseWithCompat($selectClause); + } + + /** + * Walks down a FromClause AST node, thereby generating the appropriate SQL. + */ + public function walkFromClause(FromClause $fromClause): string + { + return $this->doWalkFromClauseWithCompat($fromClause); + } + + /** + * Walks down a OrderByClause AST node, thereby generating the appropriate SQL. + */ + public function walkOrderByClause(OrderByClause $orderByClause): string + { + return $this->doWalkOrderByClauseWithCompat($orderByClause); + } + + /** + * Walks down a HavingClause AST node, thereby generating the appropriate SQL. + */ + public function walkHavingClause(HavingClause $havingClause): string + { + return $this->doWalkHavingClauseWithCompat($havingClause); + } + + /** + * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. + */ + public function walkSubselectFromClause(SubselectFromClause $subselectFromClause): string + { + return $this->doWalkSubselectFromClauseWithCompat($subselectFromClause); + } + + /** + * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. + */ + public function walkSimpleSelectClause(SimpleSelectClause $simpleSelectClause): string + { + return $this->doWalkSimpleSelectClauseWithCompat($simpleSelectClause); + } + + /** + * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. + */ + public function walkGroupByClause(GroupByClause $groupByClause): string + { + return $this->doWalkGroupByClauseWithCompat($groupByClause); + } + + /** + * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. + */ + public function walkDeleteClause(DeleteClause $deleteClause): string + { + return $this->doWalkDeleteClauseWithCompat($deleteClause); + } + + /** + * Walks down a WhereClause AST node, thereby generating the appropriate SQL. + * + * WhereClause or not, the appropriate discriminator sql is added. + */ + public function walkWhereClause(?WhereClause $whereClause): string + { + return $this->doWalkWhereClauseWithCompat($whereClause); + } + + /** + * Gets an executor that can be used to execute the result of this walker. + * + * @param SelectStatement|UpdateStatement|DeleteStatement $statement + */ + protected function doGetExecutorWithCompat($statement): AbstractSqlExecutor + { + return parent::getExecutor($statement); + } + + /** + * @param DeleteStatement|UpdateStatement|SelectStatement $AST + */ + protected function doGetFinalizerWithCompat($AST): SqlFinalizer + { + return parent::getFinalizer($AST); + } + + protected function doWalkSelectClauseWithCompat(SelectClause $selectClause): string + { + return parent::walkSelectClause($selectClause); + } + + protected function doWalkFromClauseWithCompat(FromClause $fromClause): string + { + return parent::walkFromClause($fromClause); + } + + protected function doWalkOrderByClauseWithCompat(OrderByClause $orderByClause): string + { + return parent::walkOrderByClause($orderByClause); + } + + protected function doWalkHavingClauseWithCompat(HavingClause $havingClause): string + { + return parent::walkHavingClause($havingClause); + } + + protected function doWalkSubselectFromClauseWithCompat(SubselectFromClause $subselectFromClause): string + { + return parent::walkSubselectFromClause($subselectFromClause); + } + + protected function doWalkSimpleSelectClauseWithCompat(SimpleSelectClause $simpleSelectClause): string + { + return parent::walkSimpleSelectClause($simpleSelectClause); + } + + protected function doWalkGroupByClauseWithCompat(GroupByClause $groupByClause): string + { + return parent::walkGroupByClause($groupByClause); + } + + protected function doWalkDeleteClauseWithCompat(DeleteClause $deleteClause): string + { + return parent::walkDeleteClause($deleteClause); + } + + protected function doWalkWhereClauseWithCompat(?WhereClause $whereClause): string + { + return parent::walkWhereClause($whereClause); + } +} diff --git a/src/Tool/Wrapper/AbstractWrapper.php b/src/Tool/Wrapper/AbstractWrapper.php new file mode 100644 index 0000000000..19cbcc9196 --- /dev/null +++ b/src/Tool/Wrapper/AbstractWrapper.php @@ -0,0 +1,124 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool\Wrapper; + +use Doctrine\Deprecations\Deprecation; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\UnsupportedObjectManagerException; +use Gedmo\Tool\WrapperInterface; + +/** + * Wraps entity or proxy for more convenient + * manipulation + * + * @template TClassMetadata of ClassMetadata + * @template TObject of object + * @template TObjectManager of ObjectManager + * + * @template-implements WrapperInterface + * + * @author Gediminas Morkevicius + */ +abstract class AbstractWrapper implements WrapperInterface +{ + /** + * Object metadata + * + * @var TClassMetadata + */ + protected $meta; + + /** + * Wrapped object + * + * @var TObject + */ + protected $object; + + /** + * Object manager instance + * + * @var TObjectManager + */ + protected $om; + + /** + * Wrap object factory method + * + * @param TObject $object + * @param TObjectManager $om + * + * @psalm-param object $object + * @psalm-param ObjectManager $om + * + * @throws UnsupportedObjectManagerException + * + * @return WrapperInterface + */ + public static function wrap($object, ObjectManager $om) + { + if ($om instanceof EntityManagerInterface) { + return new EntityWrapper($object, $om); + } + if ($om instanceof DocumentManager) { + return new MongoDocumentWrapper($object, $om); + } + + throw new UnsupportedObjectManagerException('Given object manager is not managed by wrapper'); + } + + /** + * @return void + */ + public static function clear() + { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2410', + 'Using "%s()" method is deprecated since gedmo/doctrine-extensions 3.5 and will be removed in version 4.0.', + __METHOD__ + ); + } + + /** + * @return TObject + */ + public function getObject() + { + return $this->object; + } + + /** + * @return TClassMetadata + */ + public function getMetadata() + { + return $this->meta; + } + + public function populate(array $data) + { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2410', + 'Using "%s()" method is deprecated since gedmo/doctrine-extensions 3.5 and will be removed in version 4.0.', + __METHOD__ + ); + + foreach ($data as $field => $value) { + $this->setPropertyValue($field, $value); + } + + return $this; + } +} diff --git a/src/Tool/Wrapper/EntityWrapper.php b/src/Tool/Wrapper/EntityWrapper.php new file mode 100644 index 0000000000..9260f3c589 --- /dev/null +++ b/src/Tool/Wrapper/EntityWrapper.php @@ -0,0 +1,125 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool\Wrapper; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; +use Gedmo\Tool\ClassUtils; + +/** + * Wraps entity or proxy for more convenient + * manipulation + * + * @template TObject of object + * + * @template-extends AbstractWrapper, TObject, EntityManagerInterface> + * + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class EntityWrapper extends AbstractWrapper +{ + /** + * Entity identifier + * + * @var array|null + */ + private $identifier; + + /** + * Wrap entity + * + * @param TObject $entity + */ + public function __construct($entity, EntityManagerInterface $em) + { + $this->om = $em; + $this->object = $entity; + $this->meta = $em->getClassMetadata(get_class($this->object)); + } + + public function getPropertyValue($property) + { + $this->initialize(); + + return $this->meta->getFieldValue($this->object, $property); + } + + public function setPropertyValue($property, $value) + { + $this->initialize(); + $this->meta->setFieldValue($this->object, $property, $value); + + return $this; + } + + public function hasValidIdentifier() + { + return null !== $this->getIdentifier(); + } + + public function getRootObjectName() + { + return $this->meta->rootEntityName; + } + + /** + * @param bool $flatten + */ + public function getIdentifier($single = true, $flatten = false) + { + $flatten = 1 < \func_num_args() && true === func_get_arg(1); + if (null === $this->identifier) { + $uow = $this->om->getUnitOfWork(); + $this->identifier = $uow->isInIdentityMap($this->object) + ? $uow->getEntityIdentifier($this->object) + : $this->meta->getIdentifierValues($this->object); + if (is_array($this->identifier) && empty($this->identifier)) { + $this->identifier = null; + } + } + if (is_array($this->identifier)) { + if ($single) { + return reset($this->identifier); + } + if ($flatten) { + $id = $this->identifier; + foreach ($id as $i => $value) { + if (is_object($value) && $this->om->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) { + $id[$i] = (new self($value, $this->om))->getIdentifier(false, true); + } + } + + return implode(' ', $id); + } + } + + return $this->identifier; + } + + public function isEmbeddedAssociation($field) + { + return false; + } + + /** + * Initialize the entity if it is proxy + * required when is detached or not initialized + * + * @return void + */ + protected function initialize() + { + if ($this->om->isUninitializedObject($this->object)) { + $this->om->initializeObject($this->object); + } + } +} diff --git a/lib/Gedmo/Tool/Wrapper/MongoDocumentWrapper.php b/src/Tool/Wrapper/MongoDocumentWrapper.php similarity index 50% rename from lib/Gedmo/Tool/Wrapper/MongoDocumentWrapper.php rename to src/Tool/Wrapper/MongoDocumentWrapper.php index c0a9f63739..e7c94dd26d 100644 --- a/lib/Gedmo/Tool/Wrapper/MongoDocumentWrapper.php +++ b/src/Tool/Wrapper/MongoDocumentWrapper.php @@ -1,38 +1,41 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tool\Wrapper; use Doctrine\ODM\MongoDB\DocumentManager; -use Doctrine\ODM\MongoDB\Proxy\Proxy; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use ProxyManager\Proxy\GhostObjectInterface; /** * Wraps document or proxy for more convenient * manipulation * + * @template TObject of object + * + * @template-extends AbstractWrapper, TObject, DocumentManager> + * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ class MongoDocumentWrapper extends AbstractWrapper { /** * Document identifier - * - * @var mixed - */ - private $identifier; - - /** - * True if document or proxy is loaded - * - * @var boolean */ - private $initialized = false; + private ?string $identifier = null; /** * Wrap document * - * @param object $document - * @param \Doctrine\ODM\MongoDB\DocumentManager $dm + * @param TObject $document */ public function __construct($document, DocumentManager $dm) { @@ -41,50 +44,40 @@ public function __construct($document, DocumentManager $dm) $this->meta = $dm->getClassMetadata(get_class($this->object)); } - /** - * {@inheritDoc} - */ public function getPropertyValue($property) { $this->initialize(); - return $this->meta->getReflectionProperty($property)->getValue($this->object); + return $this->meta->getFieldValue($this->object, $property); } - /** - * {@inheritDoc} - */ public function getRootObjectName() { return $this->meta->rootDocumentName; } - /** - * {@inheritDoc} - */ public function setPropertyValue($property, $value) { $this->initialize(); - $this->meta->getReflectionProperty($property)->setValue($this->object, $value); + $this->meta->setFieldValue($this->object, $property, $value); return $this; } - /** - * {@inheritDoc} - */ public function hasValidIdentifier() { return (bool) $this->getIdentifier(); } /** - * {@inheritDoc} + * @param bool $flatten + * + * @return string */ - public function getIdentifier($single = true) + public function getIdentifier($single = true, $flatten = false) { if (!$this->identifier) { - if ($this->object instanceof Proxy) { + if ($this->object instanceof GhostObjectInterface) { $uow = $this->om->getUnitOfWork(); if ($uow->isInIdentityMap($this->object)) { $this->identifier = (string) $uow->getDocumentIdentifier($this->object); @@ -100,38 +93,28 @@ public function getIdentifier($single = true) return $this->identifier; } + public function isEmbeddedAssociation($field) + { + return $this->getMetadata()->isSingleValuedEmbed($field); + } + /** * Initialize the document if it is proxy * required when is detached or not initialized + * + * @return void */ protected function initialize() { - if (!$this->initialized) { - if ($this->object instanceof Proxy) { - $uow = $this->om->getUnitOfWork(); - if (!$this->object->__isInitialized__) { - $persister = $uow->getDocumentPersister($this->meta->name); - $identifier = null; - if ($uow->isInIdentityMap($this->object)) { - $identifier = $this->getIdentifier(); - } else { - // this may not happen but in case - $reflProperty = new \ReflectionProperty($this->object, 'identifier'); - $reflProperty->setAccessible(true); - $identifier = $reflProperty->getValue($this->object); - } - $this->object->__isInitialized__ = true; - $persister->load($identifier, $this->object); - } - } + if (method_exists($this->om, 'isUninitializedObject') && $this->om->isUninitializedObject($this->object)) { + $this->om->initializeObject($this->object); + + return; } - } - /** - * {@inheritDoc} - */ - public function isEmbeddedAssociation($field) - { - return $this->getMetadata()->isSingleValuedEmbed($field); + // @todo: Drop support for this fallback when requiring `doctrine/mongodb-odm:^2.6 as a minimum` + if ($this->object instanceof GhostObjectInterface && !$this->object->isProxyInitialized()) { + $this->om->initializeObject($this->object); + } } } diff --git a/src/Tool/WrapperInterface.php b/src/Tool/WrapperInterface.php new file mode 100644 index 0000000000..27551df27c --- /dev/null +++ b/src/Tool/WrapperInterface.php @@ -0,0 +1,105 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tool; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; + +/** + * Interface for a wrapper of a managed object. + * + * @template-covariant TClassMetadata of ClassMetadata + * @template-covariant TObject of object + * @template-covariant TObjectManager of ObjectManager + * + * @author Gediminas Morkevicius + */ +interface WrapperInterface +{ + /** + * Get the currently wrapped object. + * + * @return TObject + */ + public function getObject(); + + /** + * Retrieves a property's value from the wrapped object. + * + * @param string $property + * + * @return mixed + */ + public function getPropertyValue($property); + + /** + * Sets a property's value on the wrapped object. + * + * @param string $property + * @param mixed $value + * + * @return $this + */ + public function setPropertyValue($property, $value); + + /** + * @deprecated since gedmo/doctrine-extensions 3.5 and to be removed in version 4.0. + * + * Populates the wrapped object with the given property values. + * + * @param array $data + * + * @return $this + */ + public function populate(array $data); + + /** + * Checks if the identifier is valid. + * + * @return bool + */ + public function hasValidIdentifier(); + + /** + * Get the object metadata. + * + * @return TClassMetadata + */ + public function getMetadata(); + + /** + * Get the object identifier, single or composite. + * + * @param bool $single + * + * @return array|mixed Array if a composite value, otherwise a single scalar + * + * @todo Uncomment the second parameter for 4.0 + */ + public function getIdentifier($single = true/* , bool $flatten = false */); + + /** + * Get the root object class name. + * + * @return string + * + * @phpstan-return class-string + */ + public function getRootObjectName(); + + /** + * Checks if an association is embedded. + * + * @param string $field + * + * @return bool + */ + public function isEmbeddedAssociation($field); +} diff --git a/lib/Gedmo/Translatable/Document/MappedSuperclass/AbstractPersonalTranslation.php b/src/Translatable/Document/MappedSuperclass/AbstractPersonalTranslation.php similarity index 70% rename from lib/Gedmo/Translatable/Document/MappedSuperclass/AbstractPersonalTranslation.php rename to src/Translatable/Document/MappedSuperclass/AbstractPersonalTranslation.php index e008bd6e47..a92c94be70 100644 --- a/lib/Gedmo/Translatable/Document/MappedSuperclass/AbstractPersonalTranslation.php +++ b/src/Translatable/Document/MappedSuperclass/AbstractPersonalTranslation.php @@ -1,54 +1,69 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Document\MappedSuperclass; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; +use Doctrine\ODM\MongoDB\Types\Type; /** * Gedmo\Translatable\Document\AbstractPersonalTranslation * * @MongoODM\MappedSuperclass */ +#[MongoODM\MappedSuperclass] abstract class AbstractPersonalTranslation { /** - * @var integer $id + * @var string|null * * @MongoODM\Id */ + #[MongoODM\Id] protected $id; /** - * @var string $locale + * @var string|null * - * @MongoODM\String + * @MongoODM\Field(type="string") */ + #[MongoODM\Field(type: Type::STRING)] protected $locale; /** - * Related entity with ManyToOne relation + * Related document with ManyToOne relation * must be mapped by user + * + * @var object|null */ protected $object; /** - * @var string $field + * @var string|null * - * @MongoODM\String + * @MongoODM\Field(type="string") */ + #[MongoODM\Field(type: Type::STRING)] protected $field; /** - * @var string $content + * @var string|null * - * @MongoODM\String + * @MongoODM\Field(type="string") */ + #[MongoODM\Field(type: Type::STRING)] protected $content; /** * Get id * - * @return integer $id + * @return string|null $id */ public function getId() { @@ -120,7 +135,7 @@ public function setObject($object) /** * Get object related * - * @return string + * @return object */ public function getObject() { diff --git a/lib/Gedmo/Translatable/Document/MappedSuperclass/AbstractTranslation.php b/src/Translatable/Document/MappedSuperclass/AbstractTranslation.php similarity index 69% rename from lib/Gedmo/Translatable/Document/MappedSuperclass/AbstractTranslation.php rename to src/Translatable/Document/MappedSuperclass/AbstractTranslation.php index 6f30d78f37..2145fe19b5 100644 --- a/lib/Gedmo/Translatable/Document/MappedSuperclass/AbstractTranslation.php +++ b/src/Translatable/Document/MappedSuperclass/AbstractTranslation.php @@ -1,62 +1,77 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Document\MappedSuperclass; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; +use Doctrine\ODM\MongoDB\Types\Type; /** -* Gedmo\Translatable\Document\MappedSuperclass\AbstractTranslation -* -* @MongoODM\MappedSuperclass -*/ + * Gedmo\Translatable\Document\MappedSuperclass\AbstractTranslation + * + * @MongoODM\MappedSuperclass + */ +#[MongoODM\MappedSuperclass] abstract class AbstractTranslation { /** - * @var integer $id + * @var int * * @MongoODM\Id */ + #[MongoODM\Id] protected $id; /** - * @var string $locale + * @var string * - * @MongoODM\String + * @MongoODM\Field(type="string") */ + #[MongoODM\Field(type: Type::STRING)] protected $locale; /** - * @var string $objectClass + * @var string * - * @MongoODM\String + * @MongoODM\Field(type="string") */ + #[MongoODM\Field(type: Type::STRING)] protected $objectClass; /** - * @var string $field + * @var string * - * @MongoODM\String + * @MongoODM\Field(type="string") */ + #[MongoODM\Field(type: Type::STRING)] protected $field; /** - * @var string $foreignKey + * @var string * - * @MongoODM\String(name="foreign_key") + * @MongoODM\Field(type="string", name="foreign_key") */ + #[MongoODM\Field(name: 'foreign_key', type: Type::STRING)] protected $foreignKey; /** - * @var string $content + * @var string * - * @MongoODM\String + * @MongoODM\Field(type="string") */ + #[MongoODM\Field(type: Type::STRING)] protected $content; /** * Get id * - * @return integer $id + * @return int $id */ public function getId() { diff --git a/lib/Gedmo/Translatable/Document/Repository/TranslationRepository.php b/src/Translatable/Document/Repository/TranslationRepository.php similarity index 54% rename from lib/Gedmo/Translatable/Document/Repository/TranslationRepository.php rename to src/Translatable/Document/Repository/TranslationRepository.php index 530da067d1..8c3b5acf2a 100644 --- a/lib/Gedmo/Translatable/Document/Repository/TranslationRepository.php +++ b/src/Translatable/Document/Repository/TranslationRepository.php @@ -1,40 +1,52 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Document\Repository; -use Gedmo\Translatable\TranslatableListener; -use Doctrine\ODM\MongoDB\DocumentRepository; -use Doctrine\ODM\MongoDB\Cursor; use Doctrine\ODM\MongoDB\DocumentManager; -use Doctrine\ODM\MongoDB\UnitOfWork; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\Repository\DocumentRepository; +use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\UnitOfWork; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Exception\RuntimeException; +use Gedmo\Exception\UnexpectedValueException; use Gedmo\Tool\Wrapper\MongoDocumentWrapper; +use Gedmo\Translatable\Document\MappedSuperclass\AbstractPersonalTranslation; use Gedmo\Translatable\Mapping\Event\Adapter\ODM as TranslatableAdapterODM; +use Gedmo\Translatable\TranslatableListener; /** * The TranslationRepository has some useful functions * to interact with translations. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @template T of object + * + * @template-extends DocumentRepository */ class TranslationRepository extends DocumentRepository { /** * Current TranslatableListener instance used * in EntityManager - * - * @var TranslatableListener */ - private $listener; + private ?TranslatableListener $listener = null; /** - * {@inheritdoc} + * @param ClassMetadata $class */ public function __construct(DocumentManager $dm, UnitOfWork $uow, ClassMetadata $class) { - if ($class->getReflectionClass()->isSubclassOf('Gedmo\Translatable\Document\MappedSuperclass\AbstractPersonalTranslation')) { - throw new \Gedmo\Exception\UnexpectedValueException('This repository is useless for personal translations'); + if ($class->getReflectionClass()->isSubclassOf(AbstractPersonalTranslation::class)) { + throw new UnexpectedValueException('This repository is useless for personal translations'); } parent::__construct($dm, $uow, $class); } @@ -54,15 +66,15 @@ public function translate($document, $field, $locale, $value) { $meta = $this->dm->getClassMetadata(get_class($document)); $listener = $this->getTranslatableListener(); - $config = $listener->getConfiguration($this->dm, $meta->name); - if (!isset($config['fields']) || !in_array($field, $config['fields'])) { - throw new \Gedmo\Exception\InvalidArgumentException("Document: {$meta->name} does not translate field - {$field}"); + $config = $listener->getConfiguration($this->dm, $meta->getName()); + if (!isset($config['fields']) || !in_array($field, $config['fields'], true)) { + throw new InvalidArgumentException("Document: {$meta->getName()} does not translate field - {$field}"); } $modRecordValue = (!$listener->getPersistDefaultLocaleTranslation() && $locale === $listener->getDefaultLocale()) || $listener->getTranslatableLocale($document, $meta, $this->getDocumentManager()) === $locale ; if ($modRecordValue) { - $meta->getReflectionProperty($field)->setValue($document, $value); + $meta->setFieldValue($document, $field, $value); $this->dm->persist($document); } else { if (isset($config['translationClass'])) { @@ -71,25 +83,30 @@ public function translate($document, $field, $locale, $value) $ea = new TranslatableAdapterODM(); $class = $listener->getTranslationClass($ea, $config['useObjectClass']); } - $foreignKey = $meta->getReflectionProperty($meta->identifier)->getValue($document); + $foreignKey = $meta->getFieldValue($document, $meta->getIdentifier()[0]); $objectClass = $config['useObjectClass']; $transMeta = $this->dm->getClassMetadata($class); - $trans = $this->findOneBy(compact('locale', 'field', 'objectClass', 'foreignKey')); + $trans = $this->findOneBy([ + 'locale' => $locale, + 'field' => $field, + 'objectClass' => $objectClass, + 'foreignKey' => $foreignKey, + ]); if (!$trans) { $trans = $transMeta->newInstance(); - $transMeta->getReflectionProperty('foreignKey')->setValue($trans, $foreignKey); - $transMeta->getReflectionProperty('objectClass')->setValue($trans, $objectClass); - $transMeta->getReflectionProperty('field')->setValue($trans, $field); - $transMeta->getReflectionProperty('locale')->setValue($trans, $locale); + $transMeta->setFieldValue($trans, 'foreignKey', $foreignKey); + $transMeta->setFieldValue($trans, 'objectClass', $objectClass); + $transMeta->setFieldValue($trans, 'field', $field); + $transMeta->setFieldValue($trans, 'locale', $locale); } $mapping = $meta->getFieldMapping($field); $type = $this->getType($mapping['type']); $transformed = $type->convertToDatabaseValue($value); - $transMeta->getReflectionProperty('content')->setValue($trans, $transformed); + $transMeta->setFieldValue($trans, 'content', $transformed); if ($this->dm->getUnitOfWork()->isInIdentityMap($document)) { $this->dm->persist($trans); } else { - $oid = spl_object_hash($document); + $oid = spl_object_id($document); $listener->addPendingTranslationInsert($oid, $trans); } } @@ -103,11 +120,11 @@ public function translate($document, $field, $locale, $value) * * @param object $document * - * @return array list of translations in locale groups + * @return array> list of translations in locale groups */ public function findTranslations($document) { - $result = array(); + $result = []; $wrapped = new MongoDocumentWrapper($document, $this->dm); if ($wrapped->hasValidIdentifier()) { $documentId = $wrapped->getIdentifier(); @@ -116,28 +133,27 @@ public function findTranslations($document) $config = $this ->getTranslatableListener() - ->getConfiguration($this->dm, get_class($document)); + ->getConfiguration($this->dm, $wrapped->getMetadata()->getName()); + + if (!$config) { + return $result; + } + + $documentClass = $config['useObjectClass']; - $translationClass = isset($config['translationClass']) ? - $config['translationClass'] : - $translationMeta->rootDocumentName; + $translationClass = $config['translationClass'] ?? $translationMeta->rootDocumentName; $qb = $this->dm->createQueryBuilder($translationClass); $q = $qb->field('foreignKey')->equals($documentId) - ->field('objectClass')->equals($wrapped->getMetadata()->rootDocumentName) + ->field('objectClass')->equals($documentClass) ->field('content')->exists(true)->notEqual(null) ->sort('locale', 'asc') ->getQuery(); $q->setHydrate(false); - $data = $q->execute(); - if ($data instanceof Cursor) { - $data = $data->toArray(); - } - if ($data && is_array($data) && count($data)) { - foreach ($data as $row) { - $result[$row['locale']][$row['field']] = $row['content']; - } + + foreach ($q->getIterator() as $row) { + $result[$row['locale']][$row['field']] = $row['content']; } } @@ -154,44 +170,47 @@ public function findTranslations($document) * @param string $value * @param string $class * - * @return object - instance of $class or null if not found + * @phpstan-param class-string $class + * + * @return object|null instance of $class or null if not found */ public function findObjectByTranslatedField($field, $value, $class) { - $document = null; $meta = $this->dm->getClassMetadata($class); - if ($meta->hasField($field)) { - $qb = $this->createQueryBuilder(); - $q = $qb->field('field')->equals($field) - ->field('objectClass')->equals($meta->rootDocumentName) - ->field('content')->equals($value) - ->getQuery(); - $q->setHydrate(false); - $result = $q->execute(); - if ($result instanceof Cursor) { - $result = $result->toArray(); - } - $id = count($result) ? $result[0]['foreignKey'] : null; - if ($id) { - $document = $this->dm->find($class, $id); - } + if (!$meta->hasField($field)) { + return null; + } + + $qb = $this->createQueryBuilder(); + $q = $qb->field('field')->equals($field) + ->field('objectClass')->equals($meta->rootDocumentName) + ->field('content')->equals($value) + ->getQuery(); + + $q->setHydrate(false); + $result = $q->getSingleResult(); + + $id = $result['foreign_key'] ?? null; + + if (null === $id) { + return null; } - return $document; + return $this->dm->find($class, $id); } /** * Loads all translations with all translatable * fields by a given document primary key * - * @param mixed $id - primary key value of document + * @param mixed $id primary key value of document * - * @return array + * @return array> */ public function findTranslationsByObjectId($id) { - $result = array(); + $result = []; if ($id) { $qb = $this->createQueryBuilder(); $q = $qb->field('foreignKey')->equals($id) @@ -200,15 +219,9 @@ public function findTranslationsByObjectId($id) ->getQuery(); $q->setHydrate(false); - $data = $q->execute(); - if ($data instanceof Cursor) { - $data = $data->toArray(); - } - if ($data && is_array($data) && count($data)) { - foreach ($data as $row) { - $result[$row['locale']][$row['field']] = $row['content']; - } + foreach ($q->getIterator() as $row) { + $result[$row['locale']][$row['field']] = $row['content']; } } @@ -218,31 +231,27 @@ public function findTranslationsByObjectId($id) /** * Get the currently used TranslatableListener * - * @throws \Gedmo\Exception\RuntimeException - if listener is not found - * - * @return TranslatableListener + * @throws RuntimeException if listener is not found */ - private function getTranslatableListener() + private function getTranslatableListener(): TranslatableListener { - if (!$this->listener) { - foreach ($this->dm->getEventManager()->getListeners() as $event => $listeners) { - foreach ($listeners as $hash => $listener) { + if (null === $this->listener) { + foreach ($this->dm->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { if ($listener instanceof TranslatableListener) { return $this->listener = $listener; } } } - throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found'); + throw new RuntimeException('The translation listener could not be found'); } return $this->listener; } - private function getType($type) + private function getType(string $type): Type { - // due to change in ODM beta 9 - return class_exists('Doctrine\ODM\MongoDB\Types\Type') ? \Doctrine\ODM\MongoDB\Types\Type::getType($type) - : \Doctrine\ODM\MongoDB\Mapping\Types\Type::getType($type); + return Type::getType($type); } } diff --git a/src/Translatable/Document/Translation.php b/src/Translatable/Document/Translation.php new file mode 100644 index 0000000000..2fdecfd33a --- /dev/null +++ b/src/Translatable/Document/Translation.php @@ -0,0 +1,34 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Translatable\Document; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Gedmo\Translatable\Document\MappedSuperclass\AbstractTranslation; +use Gedmo\Translatable\Document\Repository\TranslationRepository; + +/** + * Gedmo\Translatable\Document\Translation + * + * @ODM\Document(repositoryClass="Gedmo\Translatable\Document\Repository\TranslationRepository") + * @ODM\UniqueIndex(name="lookup_unique_idx", keys={ + * "foreign_key": "asc", + * "locale": "asc", + * "object_class": "asc", + * "field": "asc" + * }) + */ +#[ODM\Document(repositoryClass: TranslationRepository::class)] +#[ODM\UniqueIndex(name: 'lookup_unique_idx', keys: ['foreign_key' => 'asc', 'locale' => 'asc', 'object_class' => 'asc', 'field' => 'asc'])] +class Translation extends AbstractTranslation +{ + /* + * All required columns are mapped through inherited superclass + */ +} diff --git a/lib/Gedmo/Translatable/Entity/MappedSuperclass/AbstractPersonalTranslation.php b/src/Translatable/Entity/MappedSuperclass/AbstractPersonalTranslation.php similarity index 74% rename from lib/Gedmo/Translatable/Entity/MappedSuperclass/AbstractPersonalTranslation.php rename to src/Translatable/Entity/MappedSuperclass/AbstractPersonalTranslation.php index 9c529ce6af..5ff80be2da 100644 --- a/lib/Gedmo/Translatable/Entity/MappedSuperclass/AbstractPersonalTranslation.php +++ b/src/Translatable/Entity/MappedSuperclass/AbstractPersonalTranslation.php @@ -1,7 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Entity\MappedSuperclass; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** @@ -9,48 +17,57 @@ * * @ORM\MappedSuperclass */ +#[ORM\MappedSuperclass] abstract class AbstractPersonalTranslation { /** - * @var integer $id + * @var int|null * * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ + #[ORM\Column(type: Types::INTEGER)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] protected $id; /** - * @var string $locale + * @var string * * @ORM\Column(type="string", length=8) */ + #[ORM\Column(type: Types::STRING, length: 8)] protected $locale; /** - * @var string $field + * @var string * * @ORM\Column(type="string", length=32) */ + #[ORM\Column(type: Types::STRING, length: 32)] protected $field; /** * Related entity with ManyToOne relation * must be mapped by user + * + * @var object */ protected $object; /** - * @var string $content + * @var string * * @ORM\Column(type="text", nullable=true) */ + #[ORM\Column(type: Types::TEXT, nullable: true)] protected $content; /** * Get id * - * @return integer $id + * @return int|null $id */ public function getId() { @@ -108,7 +125,7 @@ public function getField() /** * Set object related * - * @param string $object + * @param object $object * * @return static */ diff --git a/lib/Gedmo/Translatable/Entity/MappedSuperclass/AbstractTranslation.php b/src/Translatable/Entity/MappedSuperclass/AbstractTranslation.php similarity index 74% rename from lib/Gedmo/Translatable/Entity/MappedSuperclass/AbstractTranslation.php rename to src/Translatable/Entity/MappedSuperclass/AbstractTranslation.php index 598f0cfdc7..f0d0bd81a5 100644 --- a/lib/Gedmo/Translatable/Entity/MappedSuperclass/AbstractTranslation.php +++ b/src/Translatable/Entity/MappedSuperclass/AbstractTranslation.php @@ -1,7 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Entity\MappedSuperclass; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** @@ -9,56 +17,65 @@ * * @ORM\MappedSuperclass */ +#[ORM\MappedSuperclass] abstract class AbstractTranslation { /** - * @var integer $id + * @var int * * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ + #[ORM\Column(type: Types::INTEGER)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] protected $id; /** - * @var string $locale + * @var string * * @ORM\Column(type="string", length=8) */ + #[ORM\Column(type: Types::STRING, length: 8)] protected $locale; /** - * @var string $objectClass + * @var string * - * @ORM\Column(name="object_class", type="string", length=255) + * @ORM\Column(name="object_class", type="string", length=191) */ + #[ORM\Column(name: 'object_class', type: Types::STRING, length: 191)] protected $objectClass; /** - * @var string $field + * @var string * * @ORM\Column(type="string", length=32) */ + #[ORM\Column(type: Types::STRING, length: 32)] protected $field; /** - * @var string $foreignKey + * @var string * * @ORM\Column(name="foreign_key", type="string", length=64) */ + #[ORM\Column(name: 'foreign_key', type: Types::STRING, length: 64)] protected $foreignKey; /** - * @var string $content + * @var string * * @ORM\Column(type="text", nullable=true) */ + #[ORM\Column(type: Types::TEXT, nullable: true)] protected $content; /** * Get id * - * @return integer $id + * @return int $id */ public function getId() { diff --git a/src/Translatable/Entity/Repository/TranslationRepository.php b/src/Translatable/Entity/Repository/TranslationRepository.php new file mode 100644 index 0000000000..54c309c95c --- /dev/null +++ b/src/Translatable/Entity/Repository/TranslationRepository.php @@ -0,0 +1,250 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Translatable\Entity\Repository; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Exception\RuntimeException; +use Gedmo\Exception\UnexpectedValueException; +use Gedmo\Tool\Wrapper\EntityWrapper; +use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation; +use Gedmo\Translatable\Mapping\Event\Adapter\ORM as TranslatableAdapterORM; +use Gedmo\Translatable\TranslatableListener; + +/** + * The TranslationRepository has some useful functions + * to interact with translations. + * + * @author Gediminas Morkevicius + * + * @phpstan-extends EntityRepository + */ +class TranslationRepository extends EntityRepository +{ + /** + * Current TranslatableListener instance used + * in EntityManager + */ + private ?TranslatableListener $listener = null; + + public function __construct(EntityManagerInterface $em, ClassMetadata $class) + { + if ($class->getReflectionClass()->isSubclassOf(AbstractPersonalTranslation::class)) { + throw new UnexpectedValueException('This repository is useless for personal translations'); + } + parent::__construct($em, $class); + } + + /** + * Makes additional translation of $entity $field into $locale + * using $value + * + * @param object $entity + * @param string $field + * @param string $locale + * @param mixed $value + * + * @throws InvalidArgumentException + * + * @return static + */ + public function translate($entity, $field, $locale, $value) + { + $meta = $this->getEntityManager()->getClassMetadata(get_class($entity)); + $listener = $this->getTranslatableListener(); + $config = $listener->getConfiguration($this->getEntityManager(), $meta->getName()); + if (!isset($config['fields']) || !in_array($field, $config['fields'], true)) { + throw new InvalidArgumentException("Entity: {$meta->getName()} does not translate field - {$field}"); + } + $needsPersist = true; + if ($locale === $listener->getTranslatableLocale($entity, $meta, $this->getEntityManager())) { + $meta->setFieldValue($entity, $field, $value); + $this->getEntityManager()->persist($entity); + } else { + if (isset($config['translationClass'])) { + $class = $config['translationClass']; + } else { + $ea = new TranslatableAdapterORM(); + $class = $listener->getTranslationClass($ea, $config['useObjectClass']); + } + $foreignKey = $meta->getFieldValue($entity, $meta->getSingleIdentifierFieldName()); + $objectClass = $config['useObjectClass']; + $transMeta = $this->getEntityManager()->getClassMetadata($class); + $trans = $this->findOneBy([ + 'locale' => $locale, + 'objectClass' => $objectClass, + 'field' => $field, + 'foreignKey' => $foreignKey, + ]); + if (!$trans) { + $trans = $transMeta->newInstance(); + $transMeta->setFieldValue($trans, 'foreignKey', $foreignKey); + $transMeta->setFieldValue($trans, 'objectClass', $objectClass); + $transMeta->setFieldValue($trans, 'field', $field); + $transMeta->setFieldValue($trans, 'locale', $locale); + } + if ($listener->getDefaultLocale() != $listener->getTranslatableLocale($entity, $meta, $this->getEntityManager()) + && $locale === $listener->getDefaultLocale()) { + $listener->setTranslationInDefaultLocale(spl_object_id($entity), $field, $trans); + $needsPersist = $listener->getPersistDefaultLocaleTranslation(); + } + $transformed = $this->getEntityManager()->getConnection()->convertToDatabaseValue($value, $meta->getTypeOfField($field)); + $transMeta->setFieldValue($trans, 'content', $transformed); + if ($needsPersist) { + if ($this->getEntityManager()->getUnitOfWork()->isInIdentityMap($entity)) { + $this->getEntityManager()->persist($trans); + } else { + $oid = spl_object_id($entity); + $listener->addPendingTranslationInsert($oid, $trans); + } + } + } + + return $this; + } + + /** + * Loads all translations with all translatable + * fields from the given entity + * + * @param object $entity Must implement Translatable + * + * @return array> list of translations in locale groups + */ + public function findTranslations($entity) + { + $result = []; + $wrapped = new EntityWrapper($entity, $this->getEntityManager()); + if ($wrapped->hasValidIdentifier()) { + $entityId = $wrapped->getIdentifier(); + $config = $this + ->getTranslatableListener() + ->getConfiguration($this->getEntityManager(), $wrapped->getMetadata()->getName()); + + if (!$config) { + return $result; + } + + $entityClass = $config['useObjectClass']; + $translationMeta = $this->getClassMetadata(); // table inheritance support + + $translationClass = $config['translationClass'] ?? $translationMeta->rootEntityName; + + $qb = $this->getEntityManager()->createQueryBuilder(); + $qb->select('trans.content, trans.field, trans.locale') + ->from($translationClass, 'trans') + ->where('trans.foreignKey = :entityId', 'trans.objectClass = :entityClass') + ->orderBy('trans.locale') + ->setParameter('entityId', $entityId) + ->setParameter('entityClass', $entityClass); + + foreach ($qb->getQuery()->toIterable([], Query::HYDRATE_ARRAY) as $row) { + $result[$row['locale']][$row['field']] = $row['content']; + } + } + + return $result; + } + + /** + * Find the entity $class by the translated field. + * Result is the first occurrence of translated field. + * Query can be slow, since there are no indexes on such + * columns + * + * @param string $field + * @param string $value + * @param string $class + * + * @phpstan-param class-string $class + * + * @return object instance of $class or null if not found + */ + public function findObjectByTranslatedField($field, $value, $class) + { + $entity = null; + $meta = $this->getEntityManager()->getClassMetadata($class); + $translationMeta = $this->getClassMetadata(); // table inheritance support + if ($meta->hasField($field)) { + $dql = "SELECT trans.foreignKey FROM {$translationMeta->rootEntityName} trans"; + $dql .= ' WHERE trans.objectClass = :class'; + $dql .= ' AND trans.field = :field'; + $dql .= ' AND trans.content = :value'; + $q = $this->getEntityManager()->createQuery($dql); + $q->setParameters([ + 'class' => $class, + 'field' => $field, + 'value' => $value, + ]); + $q->setMaxResults(1); + $id = $q->getSingleScalarResult(); + + if (null !== $id) { + $entity = $this->getEntityManager()->find($class, $id); + } + } + + return $entity; + } + + /** + * Loads all translations with all translatable + * fields by a given entity primary key + * + * @param mixed $id primary key value of an entity + * + * @return array> + */ + public function findTranslationsByObjectId($id) + { + $result = []; + if ($id) { + $translationMeta = $this->getClassMetadata(); // table inheritance support + $qb = $this->getEntityManager()->createQueryBuilder(); + $qb->select('trans.content, trans.field, trans.locale') + ->from($translationMeta->rootEntityName, 'trans') + ->where('trans.foreignKey = :entityId') + ->orderBy('trans.locale') + ->setParameter('entityId', $id); + $q = $qb->getQuery(); + + foreach ($q->toIterable([], Query::HYDRATE_ARRAY) as $row) { + $result[$row['locale']][$row['field']] = $row['content']; + } + } + + return $result; + } + + /** + * Get the currently used TranslatableListener + * + * @throws RuntimeException if listener is not found + */ + private function getTranslatableListener(): TranslatableListener + { + if (null === $this->listener) { + foreach ($this->getEntityManager()->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { + if ($listener instanceof TranslatableListener) { + return $this->listener = $listener; + } + } + } + + throw new RuntimeException('The translation listener could not be found'); + } + + return $this->listener; + } +} diff --git a/src/Translatable/Entity/Translation.php b/src/Translatable/Entity/Translation.php new file mode 100644 index 0000000000..540f1d1801 --- /dev/null +++ b/src/Translatable/Entity/Translation.php @@ -0,0 +1,36 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Translatable\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation; +use Gedmo\Translatable\Entity\Repository\TranslationRepository; + +/** + * Gedmo\Translatable\Entity\Translation + * + * @ORM\Table( + * name="ext_translations", + * options={"row_format": "DYNAMIC"}, + * uniqueConstraints={@ORM\UniqueConstraint(name="lookup_unique_idx", columns={ + * "foreign_key", "locale", "object_class", "field" + * })} + * ) + * @ORM\Entity(repositoryClass="Gedmo\Translatable\Entity\Repository\TranslationRepository") + */ +#[ORM\Entity(repositoryClass: TranslationRepository::class)] +#[ORM\Table(name: 'ext_translations', options: ['row_format' => 'DYNAMIC'])] +#[ORM\UniqueConstraint(name: 'lookup_unique_idx', columns: ['foreign_key', 'locale', 'object_class', 'field'])] +class Translation extends AbstractTranslation +{ + /* + * All required columns are mapped through inherited superclass + */ +} diff --git a/lib/Gedmo/Translatable/Hydrator/ORM/ObjectHydrator.php b/src/Translatable/Hydrator/ORM/ObjectHydrator.php similarity index 53% rename from lib/Gedmo/Translatable/Hydrator/ORM/ObjectHydrator.php rename to src/Translatable/Hydrator/ORM/ObjectHydrator.php index 529b43ddf8..a8e9983de0 100644 --- a/lib/Gedmo/Translatable/Hydrator/ORM/ObjectHydrator.php +++ b/src/Translatable/Hydrator/ORM/ObjectHydrator.php @@ -1,9 +1,19 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Hydrator\ORM; -use Gedmo\Translatable\TranslatableListener; use Doctrine\ORM\Internal\Hydration\ObjectHydrator as BaseObjectHydrator; +use Gedmo\Exception\RuntimeException; +use Gedmo\Tool\ORM\Hydration\EntityManagerRetriever; +use Gedmo\Tool\ORM\Hydration\HydratorCompat; +use Gedmo\Translatable\TranslatableListener; /** * If query uses TranslationQueryWalker and is hydrating @@ -12,24 +22,23 @@ * of the fields * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ class ObjectHydrator extends BaseObjectHydrator { + use EntityManagerRetriever; + use HydratorCompat; + /** * State of skipOnLoad for listener between hydrations * * @see ObjectHydrator::prepare() * @see ObjectHydrator::cleanup() - * - * @var bool */ - private $savedSkipOnLoad; + private ?bool $savedSkipOnLoad = null; - /** - * {@inheritdoc} - */ - protected function prepare() + protected function doPrepareWithCompat(): void { $listener = $this->getTranslatableListener(); $this->savedSkipOnLoad = $listener->isSkipOnLoad(); @@ -37,42 +46,30 @@ protected function prepare() parent::prepare(); } - /** - * {@inheritdoc} - */ - protected function cleanup() + protected function doCleanupWithCompat(): void { parent::cleanup(); $listener = $this->getTranslatableListener(); - $listener->setSkipOnLoad($this->savedSkipOnLoad !== null ? $this->savedSkipOnLoad : false); + $listener->setSkipOnLoad($this->savedSkipOnLoad ?? false); } /** * Get the currently used TranslatableListener * - * @throws \Gedmo\Exception\RuntimeException - if listener is not found + * @throws RuntimeException if listener is not found * * @return TranslatableListener */ protected function getTranslatableListener() { - $translatableListener = null; - foreach ($this->_em->getEventManager()->getListeners() as $event => $listeners) { - foreach ($listeners as $hash => $listener) { + foreach ($this->getEntityManager()->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { if ($listener instanceof TranslatableListener) { - $translatableListener = $listener; - break; + return $listener; } } - if ($translatableListener) { - break; - } - } - - if (is_null($translatableListener)) { - throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found'); } - return $translatableListener; + throw new RuntimeException('The translation listener could not be found'); } } diff --git a/lib/Gedmo/Translatable/Hydrator/ORM/SimpleObjectHydrator.php b/src/Translatable/Hydrator/ORM/SimpleObjectHydrator.php similarity index 54% rename from lib/Gedmo/Translatable/Hydrator/ORM/SimpleObjectHydrator.php rename to src/Translatable/Hydrator/ORM/SimpleObjectHydrator.php index 2e85c9f06f..0410018c46 100644 --- a/lib/Gedmo/Translatable/Hydrator/ORM/SimpleObjectHydrator.php +++ b/src/Translatable/Hydrator/ORM/SimpleObjectHydrator.php @@ -1,9 +1,19 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Hydrator\ORM; -use Gedmo\Translatable\TranslatableListener; use Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator as BaseSimpleObjectHydrator; +use Gedmo\Exception\RuntimeException; +use Gedmo\Tool\ORM\Hydration\EntityManagerRetriever; +use Gedmo\Tool\ORM\Hydration\HydratorCompat; +use Gedmo\Translatable\TranslatableListener; /** * If query uses TranslationQueryWalker and is hydrating @@ -12,24 +22,23 @@ * of the fields * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ class SimpleObjectHydrator extends BaseSimpleObjectHydrator { + use EntityManagerRetriever; + use HydratorCompat; + /** * State of skipOnLoad for listener between hydrations * * @see SimpleObjectHydrator::prepare() * @see SimpleObjectHydrator::cleanup() - * - * @var bool */ - private $savedSkipOnLoad; + private ?bool $savedSkipOnLoad = null; - /** - * {@inheritdoc} - */ - protected function prepare() + protected function doPrepareWithCompat(): void { $listener = $this->getTranslatableListener(); $this->savedSkipOnLoad = $listener->isSkipOnLoad(); @@ -37,42 +46,30 @@ protected function prepare() parent::prepare(); } - /** - * {@inheritdoc} - */ - protected function cleanup() + protected function doCleanupWithCompat(): void { parent::cleanup(); $listener = $this->getTranslatableListener(); - $listener->setSkipOnLoad($this->savedSkipOnLoad !== null ? $this->savedSkipOnLoad : false); + $listener->setSkipOnLoad($this->savedSkipOnLoad ?? false); } /** * Get the currently used TranslatableListener * - * @throws \Gedmo\Exception\RuntimeException - if listener is not found + * @throws RuntimeException if listener is not found * * @return TranslatableListener */ protected function getTranslatableListener() { - $translatableListener = null; - foreach ($this->_em->getEventManager()->getListeners() as $event => $listeners) { - foreach ($listeners as $hash => $listener) { + foreach ($this->getEntityManager()->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { if ($listener instanceof TranslatableListener) { - $translatableListener = $listener; - break; + return $listener; } } - if ($translatableListener) { - break; - } - } - - if (is_null($translatableListener)) { - throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found'); } - return $translatableListener; + throw new RuntimeException('The translation listener could not be found'); } } diff --git a/src/Translatable/Mapping/Driver/Annotation.php b/src/Translatable/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..83cb4ecb45 --- /dev/null +++ b/src/Translatable/Mapping/Driver/Annotation.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Translatable\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the translatable extension which reads extended metadata from annotations on a translatable class. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/lib/Gedmo/Translatable/Mapping/Driver/Annotation.php b/src/Translatable/Mapping/Driver/Attribute.php similarity index 56% rename from lib/Gedmo/Translatable/Mapping/Driver/Annotation.php rename to src/Translatable/Mapping/Driver/Attribute.php index 5d36fec68c..da78f83f46 100644 --- a/lib/Gedmo/Translatable/Mapping/Driver/Annotation.php +++ b/src/Translatable/Mapping/Driver/Attribute.php @@ -1,89 +1,113 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Mapping\Driver; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Doctrine\ORM\Mapping\EmbeddedClassMapping; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Language; +use Gedmo\Mapping\Annotation\Locale; +use Gedmo\Mapping\Annotation\Translatable; +use Gedmo\Mapping\Annotation\TranslationEntity; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an annotation mapping driver for Translatable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Translatable - * extension. + * Mapping driver for the translatable extension which reads extended metadata from annotations on a translatable class. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ -class Annotation extends AbstractAnnotationDriver +class Attribute extends AbstractAnnotationDriver { /** - * Annotation to identity translation entity to be used for translation storage + * Mapping object to configure the translation model for a translatable class. */ - const ENTITY_CLASS = 'Gedmo\\Mapping\\Annotation\\TranslationEntity'; + public const ENTITY_CLASS = TranslationEntity::class; /** - * Annotation to identify field as translatable + * Mapping object to identify a field as translatable in a translatable class. */ - const TRANSLATABLE = 'Gedmo\\Mapping\\Annotation\\Translatable'; + public const TRANSLATABLE = Translatable::class; /** - * Annotation to identify field which can store used locale or language - * alias is LANGUAGE + * Mapping object to identify the field which stores the locale or language for the translation. + * + * This object is an alias of {@see self::LANGUAGE} */ - const LOCALE = 'Gedmo\\Mapping\\Annotation\\Locale'; + public const LOCALE = Locale::class; /** - * Annotation to identify field which can store used locale or language - * alias is LOCALE + * Mapping object to identify the field which stores the locale or language for the translation. + * + * This object is an alias of {@see self::LOCALE} */ - const LANGUAGE = 'Gedmo\\Mapping\\Annotation\\Language'; + public const LANGUAGE = Language::class; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { $class = $this->getMetaReflectionClass($meta); + // class annotations if ($annot = $this->reader->getClassAnnotation($class, self::ENTITY_CLASS)) { + \assert($annot instanceof TranslationEntity); + if (!$cl = $this->getRelatedClassName($meta, $annot->class)) { throw new InvalidMappingException("Translation class: {$annot->class} does not exist."); } + $config['translationClass'] = $cl; } // property annotations foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() || - $meta->isInheritedField($property->name) || - isset($meta->associationMappings[$property->name]['inherited']) + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) ) { continue; } + // translatable property if ($translatable = $this->reader->getPropertyAnnotation($property, self::TRANSLATABLE)) { + \assert($translatable instanceof Translatable); + $field = $property->getName(); + if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find translatable [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find translatable [{$field}] as mapped property in entity - {$meta->getName()}"); } + // fields cannot be overrided and throws mapping exception $config['fields'][] = $field; + if (isset($translatable->fallback)) { $config['fallback'][$field] = $translatable->fallback; } } + // locale property if ($this->reader->getPropertyAnnotation($property, self::LOCALE)) { $field = $property->getName(); + if ($meta->hasField($field)) { - throw new InvalidMappingException("Locale field [{$field}] should not be mapped as column property in entity - {$meta->name}, since it makes no sense"); + throw new InvalidMappingException("Locale field [{$field}] should not be mapped as column property in entity - {$meta->getName()}, since it makes no sense"); } + $config['locale'] = $field; } elseif ($this->reader->getPropertyAnnotation($property, self::LANGUAGE)) { $field = $property->getName(); + if ($meta->hasField($field)) { - throw new InvalidMappingException("Language field [{$field}] should not be mapped as column property in entity - {$meta->name}, since it makes no sense"); + throw new InvalidMappingException("Language field [{$field}] should not be mapped as column property in entity - {$meta->getName()}, since it makes no sense"); } + $config['locale'] = $field; } } @@ -91,12 +115,22 @@ public function readExtendedMetadata($meta, array &$config) // Embedded entity if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) { foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) { - $embeddedClass = new \ReflectionClass($embeddedClassInfo['class']); + if ($meta->isInheritedEmbeddedClass($propertyName)) { + continue; + } + + /** Remove conditional when ORM 2.x is no longer supported. */ + $className = ($embeddedClassInfo instanceof EmbeddedClassMapping) ? $embeddedClassInfo->class : $embeddedClassInfo['class']; + $embeddedClass = new \ReflectionClass($className); + foreach ($embeddedClass->getProperties() as $embeddedProperty) { if ($translatable = $this->reader->getPropertyAnnotation($embeddedProperty, self::TRANSLATABLE)) { - $field = $propertyName . '.' . $embeddedProperty->getName(); + \assert($translatable instanceof Translatable); + + $field = $propertyName.'.'.$embeddedProperty->getName(); $config['fields'][] = $field; + if (isset($translatable->fallback)) { $config['fallback'][$field] = $translatable->fallback; } @@ -106,9 +140,11 @@ public function readExtendedMetadata($meta, array &$config) } if (!$meta->isMappedSuperclass && $config) { - if (is_array($meta->identifier) && count($meta->identifier) > 1) { - throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->name}"); + if (is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->getName()}"); } } + + return $config; } } diff --git a/src/Translatable/Mapping/Driver/Xml.php b/src/Translatable/Mapping/Driver/Xml.php new file mode 100644 index 0000000000..0f02b7bc12 --- /dev/null +++ b/src/Translatable/Mapping/Driver/Xml.php @@ -0,0 +1,133 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Translatable\Mapping\Driver; + +use Doctrine\ORM\Mapping\EmbeddedClassMapping; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver\Xml as BaseXml; + +/** + * This is a xml mapping driver for Translatable + * behavioral extension. Used for extraction of extended + * metadata from xml specifically for Translatable + * extension. + * + * @author Gediminas Morkevicius + * @author Miha Vrhovnik + * + * @internal + */ +class Xml extends BaseXml +{ + public function readExtendedMetadata($meta, array &$config) + { + /** + * @var \SimpleXmlElement + */ + $xml = $this->_getMapping($meta->getName()); + $xmlDoctrine = $xml; + + $xml = $xml->children(self::GEDMO_NAMESPACE_URI); + + if ('entity' === $xmlDoctrine->getName() || 'mapped-superclass' === $xmlDoctrine->getName()) { + if ($xml->count() && isset($xml->translation)) { + /** + * @var \SimpleXmlElement + */ + $data = $xml->translation; + if ($this->_isAttributeSet($data, 'locale')) { + $config['locale'] = $this->_getAttribute($data, 'locale'); + } elseif ($this->_isAttributeSet($data, 'language')) { + $config['locale'] = $this->_getAttribute($data, 'language'); + } + if ($this->_isAttributeSet($data, 'entity')) { + $entity = $this->_getAttribute($data, 'entity'); + if (!$cl = $this->getRelatedClassName($meta, $entity)) { + throw new InvalidMappingException("Translation entity class: {$entity} does not exist."); + } + $config['translationClass'] = $cl; + } + } + } + + if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) { + foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) { + if ($meta->isInheritedEmbeddedClass($propertyName)) { + continue; + } + + /** Remove conditional when ORM 2.x is no longer supported. */ + $className = ($embeddedClassInfo instanceof EmbeddedClassMapping) ? $embeddedClassInfo->class : $embeddedClassInfo['class']; + $xmlEmbeddedClass = $this->_getMapping($className); + $config = $this->inspectElementsForTranslatableFields($xmlEmbeddedClass, $config, $propertyName); + } + } + + if ($xmlDoctrine->{'attribute-overrides'}->count() > 0) { + foreach ($xmlDoctrine->{'attribute-overrides'}->{'attribute-override'} as $overrideMapping) { + $config = $this->buildFieldConfiguration($this->_getAttribute($overrideMapping, 'name'), $overrideMapping->field, $config); + } + } + + $config = $this->inspectElementsForTranslatableFields($xmlDoctrine, $config); + + if (!$meta->isMappedSuperclass && $config) { + if (is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->getName()}"); + } + } + + return $config; + } + + /** + * @param array $config + * + * @return array + */ + private function inspectElementsForTranslatableFields(\SimpleXMLElement $xml, array $config, ?string $prefix = null): array + { + if (!isset($xml->field)) { + return $config; + } + + foreach ($xml->field as $mapping) { + $mappingDoctrine = $mapping; + + $fieldName = $this->_getAttribute($mappingDoctrine, 'name'); + if (null !== $prefix) { + $fieldName = $prefix.'.'.$fieldName; + } + $config = $this->buildFieldConfiguration($fieldName, $mapping, $config); + } + + return $config; + } + + /** + * @param array $config + * + * @return array + */ + private function buildFieldConfiguration(string $fieldName, \SimpleXMLElement $mapping, array $config): array + { + $mapping = $mapping->children(self::GEDMO_NAMESPACE_URI); + if ($mapping->count() > 0 && isset($mapping->translatable)) { + $config['fields'][] = $fieldName; + /** @var \SimpleXmlElement $data */ + $data = $mapping->translatable; + if ($this->_isAttributeSet($data, 'fallback')) { + $config['fallback'][$fieldName] = $this->_getBooleanAttribute($data, 'fallback'); + } + } + + return $config; + } +} diff --git a/lib/Gedmo/Translatable/Mapping/Driver/Yaml.php b/src/Translatable/Mapping/Driver/Yaml.php similarity index 52% rename from lib/Gedmo/Translatable/Mapping/Driver/Yaml.php rename to src/Translatable/Mapping/Driver/Yaml.php index 3ba76062e1..dea0516339 100644 --- a/lib/Gedmo/Translatable/Mapping/Driver/Yaml.php +++ b/src/Translatable/Mapping/Driver/Yaml.php @@ -1,10 +1,17 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Mapping\Driver; -use Gedmo\Mapping\Driver\File; -use Gedmo\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Translatable @@ -13,22 +20,23 @@ * extension. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ class Yaml extends File implements Driver { /** * File extension + * * @var string */ protected $_extension = '.dcm.yml'; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['gedmo'])) { $classMapping = $mapping['gedmo']; @@ -45,32 +53,51 @@ public function readExtendedMetadata($meta, array &$config) $config['locale'] = $classMapping['translation']['language']; } } + if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { - if (isset($fieldMapping['gedmo'])) { - if (in_array('translatable', $fieldMapping['gedmo']) || isset($fieldMapping['gedmo']['translatable'])) { - // fields cannot be overrided and throws mapping exception - $config['fields'][] = $field; - if (isset($fieldMapping['gedmo']['translatable']['fallback'])) { - $config['fallback'][$field] = $fieldMapping['gedmo']['translatable']['fallback']; - } - } - } + $config = $this->buildFieldConfiguration($field, $fieldMapping, $config); + } + } + + if (isset($mapping['attributeOverride'])) { + foreach ($mapping['attributeOverride'] as $field => $overrideMapping) { + $config = $this->buildFieldConfiguration($field, $overrideMapping, $config); } } if (!$meta->isMappedSuperclass && $config) { - if (is_array($meta->identifier) && count($meta->identifier) > 1) { - throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->name}"); + if (is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->getName()}"); } } + + return $config; } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } + + /** + * @param array $fieldMapping + * @param array $config + * + * @return array + */ + private function buildFieldConfiguration(string $field, array $fieldMapping, array $config): array + { + if (isset($fieldMapping['gedmo'])) { + if (in_array('translatable', $fieldMapping['gedmo'], true) || isset($fieldMapping['gedmo']['translatable'])) { + // fields cannot be overrided and throws mapping exception + $config['fields'][] = $field; + if (isset($fieldMapping['gedmo']['translatable']['fallback'])) { + $config['fallback'][$field] = $fieldMapping['gedmo']['translatable']['fallback']; + } + } + } + + return $config; + } } diff --git a/lib/Gedmo/Translatable/Mapping/Event/Adapter/ODM.php b/src/Translatable/Mapping/Event/Adapter/ODM.php similarity index 73% rename from lib/Gedmo/Translatable/Mapping/Event/Adapter/ODM.php rename to src/Translatable/Mapping/Event/Adapter/ODM.php index 6c2ad4ebeb..be64177ce9 100644 --- a/lib/Gedmo/Translatable/Mapping/Event/Adapter/ODM.php +++ b/src/Translatable/Mapping/Event/Adapter/ODM.php @@ -1,11 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Mapping\Event\Adapter; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\Types\Type; +use Gedmo\Exception\RuntimeException; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\Tool\Wrapper\AbstractWrapper; -use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo; -use Doctrine\ODM\MongoDB\Cursor; +use Gedmo\Tool\Wrapper\MongoDocumentWrapper; +use Gedmo\Translatable\Document\MappedSuperclass\AbstractPersonalTranslation; +use Gedmo\Translatable\Document\Translation; use Gedmo\Translatable\Mapping\Event\TranslatableAdapter; /** @@ -13,56 +24,47 @@ * for Translatable behavior * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ODM extends BaseAdapterODM implements TranslatableAdapter { - /** - * {@inheritDoc} - */ public function usesPersonalTranslation($translationClassName) { return $this ->getObjectManager() ->getClassMetadata($translationClassName) ->getReflectionClass() - ->isSubclassOf('Gedmo\Translatable\Document\MappedSuperclass\AbstractPersonalTranslation') + ->isSubclassOf(AbstractPersonalTranslation::class) ; } - /** - * {@inheritDoc} - */ public function getDefaultTranslationClass() { - return 'Gedmo\\Translatable\\Document\\Translation'; + return Translation::class; } - /** - * {@inheritDoc} - */ public function loadTranslations($object, $translationClass, $locale, $objectClass) { $dm = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $dm); - $result = array(); + assert($wrapped instanceof MongoDocumentWrapper); + $result = []; if ($this->usesPersonalTranslation($translationClass)) { // first try to load it using collection foreach ($wrapped->getMetadata()->fieldMappings as $mapping) { $isRightCollection = isset($mapping['association']) - && $mapping['association'] === ClassMetadataInfo::REFERENCE_MANY + && ClassMetadata::REFERENCE_MANY === $mapping['association'] && $mapping['targetDocument'] === $translationClass - && $mapping['mappedBy'] === 'object' + && 'object' === $mapping['mappedBy'] ; if ($isRightCollection) { $collection = $wrapped->getPropertyValue($mapping['fieldName']); foreach ($collection as $trans) { if ($trans->getLocale() === $locale) { - $result[] = array( + $result[] = [ 'field' => $trans->getField(), 'content' => $trans->getContent(), - ); + ]; } } @@ -87,17 +89,10 @@ public function loadTranslations($object, $translationClass, $locale, $objectCla ; } $q->setHydrate(false); - $result = $q->execute(); - if ($result instanceof Cursor) { - $result = $result->toArray(); - } - return $result; + return $q->getIterator()->toArray(); } - /** - * {@inheritDoc} - */ public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $translationClass, $objectClass) { $dm = $this->getObjectManager(); @@ -114,17 +109,10 @@ public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $tran $qb->field('objectClass')->equals($objectClass); } $q = $qb->getQuery(); - $result = $q->execute(); - if ($result instanceof Cursor) { - $result = current($result->toArray()); - } - return $result; + return $q->getSingleResult(); } - /** - * {@inheritDoc} - */ public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transClass, $objectClass) { $dm = $this->getObjectManager(); @@ -143,51 +131,46 @@ public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transCla return $q->execute(); } - /** - * {@inheritDoc} - */ public function insertTranslationRecord($translation) { $dm = $this->getObjectManager(); $meta = $dm->getClassMetadata(get_class($translation)); - $collection = $dm->getDocumentCollection($meta->name); - $data = array(); + $collection = $dm->getDocumentCollection($meta->getName()); + $data = []; foreach ($meta->getReflectionProperties() as $fieldName => $reflProp) { if (!$meta->isIdentifier($fieldName)) { - $data[$meta->fieldMappings[$fieldName]['name']] = $reflProp->getValue($translation); + $data[$meta->getFieldMapping($fieldName)['name']] = $reflProp->getValue($translation); } } - if (!$collection->insert($data)) { - throw new \Gedmo\Exception\RuntimeException('Failed to insert new Translation record'); + $insertResult = $collection->insertOne($data); + + if (false === $insertResult->isAcknowledged()) { + throw new RuntimeException('Failed to insert new Translation record'); } } - /** - * {@inheritDoc} - */ public function getTranslationValue($object, $field, $value = false) { $dm = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $dm); + assert($wrapped instanceof MongoDocumentWrapper); $meta = $wrapped->getMetadata(); $mapping = $meta->getFieldMapping($field); $type = $this->getType($mapping['type']); - if ($value === false) { + if (false === $value) { $value = $wrapped->getPropertyValue($field); } return $type->convertToDatabaseValue($value); } - /** - * {@inheritDoc} - */ public function setTranslationValue($object, $field, $value) { $dm = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $dm); + assert($wrapped instanceof MongoDocumentWrapper); $meta = $wrapped->getMetadata(); $mapping = $meta->getFieldMapping($field); $type = $this->getType($mapping['type']); @@ -196,10 +179,8 @@ public function setTranslationValue($object, $field, $value) $wrapped->setPropertyValue($field, $value); } - private function getType($type) + private function getType(string $type): Type { - // due to change in ODM beta 9 - return class_exists('Doctrine\ODM\MongoDB\Types\Type') ? \Doctrine\ODM\MongoDB\Types\Type::getType($type) - : \Doctrine\ODM\MongoDB\Mapping\Types\Type::getType($type); + return Type::getType($type); } } diff --git a/lib/Gedmo/Translatable/Mapping/Event/Adapter/ORM.php b/src/Translatable/Mapping/Event/Adapter/ORM.php similarity index 73% rename from lib/Gedmo/Translatable/Mapping/Event/Adapter/ORM.php rename to src/Translatable/Mapping/Event/Adapter/ORM.php index d5c202824c..65afdf4649 100644 --- a/lib/Gedmo/Translatable/Mapping/Event/Adapter/ORM.php +++ b/src/Translatable/Mapping/Event/Adapter/ORM.php @@ -1,71 +1,76 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Mapping\Event\Adapter; use Doctrine\Common\Proxy\Proxy; use Doctrine\DBAL\Types\Type; -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping\ClassMetadata as EntityClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataInfo as LegacyEntityClassMetadata; +use Gedmo\Exception\RuntimeException; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; -use Gedmo\Translatable\Mapping\Event\TranslatableAdapter; use Gedmo\Tool\Wrapper\AbstractWrapper; +use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Mapping\Event\TranslatableAdapter; /** * Doctrine event adapter for ORM adapted * for Translatable behavior * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ORM extends BaseAdapterORM implements TranslatableAdapter { - /** - * {@inheritDoc} - */ public function usesPersonalTranslation($translationClassName) { return $this ->getObjectManager() ->getClassMetadata($translationClassName) ->getReflectionClass() - ->isSubclassOf('Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation') + ->isSubclassOf(AbstractPersonalTranslation::class) ; } - /** - * {@inheritDoc} - */ public function getDefaultTranslationClass() { - return 'Gedmo\\Translatable\\Entity\\Translation'; + return Translation::class; } - /** - * {@inheritDoc} - */ public function loadTranslations($object, $translationClass, $locale, $objectClass) { $em = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $em); - $result = array(); + $result = []; if ($this->usesPersonalTranslation($translationClass)) { // first try to load it using collection $found = false; - foreach ($wrapped->getMetadata()->associationMappings as $assoc) { + $metadata = $wrapped->getMetadata(); + assert($metadata instanceof EntityClassMetadata || $metadata instanceof LegacyEntityClassMetadata); + foreach ($metadata->getAssociationMappings() as $assoc) { $isRightCollection = $assoc['targetEntity'] === $translationClass - && $assoc['mappedBy'] === 'object' - && $assoc['type'] === ClassMetadataInfo::ONE_TO_MANY + && 'object' === $assoc['mappedBy'] + && EntityClassMetadata::ONE_TO_MANY === $assoc['type'] ; if ($isRightCollection) { $collection = $wrapped->getPropertyValue($assoc['fieldName']); foreach ($collection as $trans) { if ($trans->getLocale() === $locale) { - $result[] = array( + $result[] = [ 'field' => $trans->getField(), 'content' => $trans->getContent(), - ); + ]; } } $found = true; + break; } } @@ -76,7 +81,10 @@ public function loadTranslations($object, $translationClass, $locale, $objectCla $dql .= ' AND t.object = :object'; $q = $em->createQuery($dql); - $q->setParameters(compact('object', 'locale')); + $q->setParameters([ + 'object' => $object, + 'locale' => $locale, + ]); $result = $q->getArrayResult(); } } else { @@ -89,39 +97,17 @@ public function loadTranslations($object, $translationClass, $locale, $objectCla $dql .= ' AND t.objectClass = :objectClass'; // fetch results $q = $em->createQuery($dql); - $q->setParameters(compact('objectId', 'locale', 'objectClass')); + $q->setParameters([ + 'objectId' => $objectId, + 'locale' => $locale, + 'objectClass' => $objectClass, + ]); $result = $q->getArrayResult(); } return $result; } - /** - * Transforms foreigh key of translation to appropriate PHP value - * to prevent database level cast - * - * @param $key - foreign key value - * @param $className - translation class name - * @return transformed foreign key - */ - private function foreignKey($key, $className) - { - $em = $this->getObjectManager(); - $meta = $em->getClassMetadata($className); - $type = Type::getType($meta->getTypeOfField('foreignKey')); - switch ($type->getName()) { - case Type::BIGINT: - case Type::INTEGER: - case Type::SMALLINT: - return intval($key); - default: - return (string)$key; - } - } - - /** - * {@inheritDoc} - */ public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $translationClass, $objectClass) { $em = $this->getObjectManager(); @@ -139,7 +125,7 @@ public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $tran } else { $objectId = $this->foreignKey($wrapped->getIdentifier(), $translationClass); $isRequestedTranslation = $trans->getForeignKey() === $objectId - && $trans->getObjectClass() === $wrapped->getMetadata()->name + && $trans->getObjectClass() === $wrapped->getMetadata()->getName() ; } } @@ -157,8 +143,10 @@ public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $tran 'trans.locale = :locale', 'trans.field = :field' ) + ->setParameter('locale', $locale) + ->setParameter('field', $field) ; - $qb->setParameters(compact('locale', 'field')); + if ($this->usesPersonalTranslation($translationClass)) { $qb->andWhere('trans.object = :object'); if ($wrapped->getIdentifier()) { @@ -174,18 +162,10 @@ public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $tran } $q = $qb->getQuery(); $q->setMaxResults(1); - $result = $q->getResult(); - if ($result) { - return array_shift($result); - } - - return null; + return $q->getOneOrNullResult(); } - /** - * {@inheritDoc} - */ public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transClass, $objectClass) { $qb = $this @@ -208,14 +188,11 @@ public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transCla return $qb->getQuery()->getSingleScalarResult(); } - /** - * {@inheritDoc} - */ public function insertTranslationRecord($translation) { $em = $this->getObjectManager(); $meta = $em->getClassMetadata(get_class($translation)); - $data = array(); + $data = []; foreach ($meta->getReflectionProperties() as $fieldName => $reflProp) { if (!$meta->isIdentifier($fieldName)) { @@ -225,36 +202,56 @@ public function insertTranslationRecord($translation) $table = $meta->getTableName(); if (!$em->getConnection()->insert($table, $data)) { - throw new \Gedmo\Exception\RuntimeException('Failed to insert new Translation record'); + throw new RuntimeException('Failed to insert new Translation record'); } } - /** - * {@inheritDoc} - */ public function getTranslationValue($object, $field, $value = false) { $em = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $em); $meta = $wrapped->getMetadata(); - $type = Type::getType($meta->getTypeOfField($field)); - if ($value === false) { + + if (false === $value) { $value = $wrapped->getPropertyValue($field); } - return $type->convertToDatabaseValue($value, $em->getConnection()->getDatabasePlatform()); + return $em->getConnection()->convertToDatabaseValue($value, $meta->getTypeOfField($field)); } - /** - * {@inheritDoc} - */ public function setTranslationValue($object, $field, $value) { $em = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $em); $meta = $wrapped->getMetadata(); - $type = Type::getType($meta->getTypeOfField($field)); - $value = $type->convertToPHPValue($value, $em->getConnection()->getDatabasePlatform()); + $value = $em->getConnection()->convertToPHPValue($value, $meta->getTypeOfField($field)); $wrapped->setPropertyValue($field, $value); } + + /** + * Transforms foreign key of translation to appropriate PHP value + * to prevent database level cast + * + * @param mixed $key foreign key value + * @param string $className translation class name + * + * @phpstan-param class-string $className translation class name + * + * @return int|string transformed foreign key + */ + private function foreignKey($key, string $className) + { + $em = $this->getObjectManager(); + $meta = $em->getClassMetadata($className); + $type = Type::getType($meta->getTypeOfField('foreignKey')); + + switch (Type::lookupName($type)) { + case Types::BIGINT: + case Types::INTEGER: + case Types::SMALLINT: + return (int) $key; + default: + return (string) $key; + } + } } diff --git a/src/Translatable/Mapping/Event/TranslatableAdapter.php b/src/Translatable/Mapping/Event/TranslatableAdapter.php new file mode 100644 index 0000000000..4d4503f6b0 --- /dev/null +++ b/src/Translatable/Mapping/Event/TranslatableAdapter.php @@ -0,0 +1,119 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Translatable\Mapping\Event; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Mapping\Event\AdapterInterface; +use Gedmo\Tool\Wrapper\AbstractWrapper; + +/** + * Doctrine event adapter for the Translatable extension. + * + * @author Gediminas Morkevicius + */ +interface TranslatableAdapter extends AdapterInterface +{ + /** + * Checks if the given translation class is a subclass of the personal translation class. + * + * @param string $translationClassName + * + * @phpstan-param class-string $translationClassName + * + * @return bool + */ + public function usesPersonalTranslation($translationClassName); + + /** + * Get the default translation class used to store translations. + * + * @return string + * + * @phpstan-return class-string + */ + public function getDefaultTranslationClass(); + + /** + * Load the translations for a given object. + * + * @param object $object + * @param string $translationClass + * @param string $locale + * @param string $objectClass + * + * @phpstan-param class-string $translationClass + * @phpstan-param class-string $objectClass + * + * @return array> + */ + public function loadTranslations($object, $translationClass, $locale, $objectClass); + + /** + * Search for an existing translation record. + * + * @param string $locale + * @param string $field + * @param string $translationClass + * @param string $objectClass + * + * @phpstan-param AbstractWrapper, object, ObjectManager> $wrapped + * @phpstan-param class-string $translationClass + * @phpstan-param class-string $objectClass + * + * @return mixed null if nothing is found, translation object otherwise + */ + public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $translationClass, $objectClass); + + /** + * Removes all associated translations for the given object. + * + * @param string $transClass + * @param string $objectClass + * + * @phpstan-param AbstractWrapper, object, ObjectManager> $wrapped + * @phpstan-param class-string $transClass + * @phpstan-param class-string $objectClass + * + * @return int + */ + public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transClass, $objectClass); + + /** + * Inserts the translation record. + * + * @param object $translation + * + * @return void + */ + public function insertTranslationRecord($translation); + + /** + * Get the transformed value for translation storage. + * + * @param object $object + * @param string $field + * @param mixed $value + * + * @return mixed + */ + public function getTranslationValue($object, $field, $value = false); + + /** + * Transform the value from the database for translation + * + * @param object $object + * @param string $field + * @param mixed $value + * + * @return void + */ + public function setTranslationValue($object, $field, $value); +} diff --git a/lib/Gedmo/Translatable/Query/TreeWalker/TranslationWalker.php b/src/Translatable/Query/TreeWalker/TranslationWalker.php similarity index 51% rename from lib/Gedmo/Translatable/Query/TreeWalker/TranslationWalker.php rename to src/Translatable/Query/TreeWalker/TranslationWalker.php index 1cc2ebcdfa..6d006fc25c 100644 --- a/lib/Gedmo/Translatable/Query/TreeWalker/TranslationWalker.php +++ b/src/Translatable/Query/TreeWalker/TranslationWalker.php @@ -1,18 +1,46 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable\Query\TreeWalker; -use Gedmo\Translatable\Mapping\Event\Adapter\ORM as TranslatableEventAdapter; -use Gedmo\Translatable\TranslatableListener; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Types\Type; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; -use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\AST\DeleteStatement; +use Doctrine\ORM\Query\AST\FromClause; +use Doctrine\ORM\Query\AST\GroupByClause; +use Doctrine\ORM\Query\AST\HavingClause; +use Doctrine\ORM\Query\AST\Join; +use Doctrine\ORM\Query\AST\Node; +use Doctrine\ORM\Query\AST\OrderByClause; +use Doctrine\ORM\Query\AST\RangeVariableDeclaration; +use Doctrine\ORM\Query\AST\SelectClause; use Doctrine\ORM\Query\AST\SelectStatement; +use Doctrine\ORM\Query\AST\SimpleSelectClause; +use Doctrine\ORM\Query\AST\SubselectFromClause; +use Doctrine\ORM\Query\AST\UpdateStatement; +use Doctrine\ORM\Query\AST\WhereClause; +use Doctrine\ORM\Query\Exec\AbstractSqlExecutor; use Doctrine\ORM\Query\Exec\SingleSelectExecutor; -use Doctrine\ORM\Query\AST\RangeVariableDeclaration; -use Doctrine\ORM\Query\AST\Join; -use Doctrine\DBAL\Platforms\MySqlPlatform; -use Doctrine\DBAL\Platforms\PostgreSqlPlatform; +use Doctrine\ORM\Query\Exec\SingleSelectSqlFinalizer; +use Doctrine\ORM\Query\Exec\SqlFinalizer; +use Doctrine\ORM\Query\SqlOutputWalker; +use Gedmo\Exception\RuntimeException; +use Gedmo\Tool\ORM\Walker\SqlWalkerCompat; +use Gedmo\Translatable\Hydrator\ORM\ObjectHydrator; +use Gedmo\Translatable\Hydrator\ORM\SimpleObjectHydrator; +use Gedmo\Translatable\Mapping\Event\Adapter\ORM as TranslatableEventAdapter; +use Gedmo\Translatable\TranslatableListener; /** * The translation sql output walker makes it possible @@ -25,70 +53,70 @@ * of the fields. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ -class TranslationWalker extends SqlWalker +class TranslationWalker extends SqlOutputWalker { + use SqlWalkerCompat; + /** * Name for translation fallback hint * * @internal */ - const HINT_TRANSLATION_FALLBACKS = '__gedmo.translatable.stored.fallbacks'; + public const HINT_TRANSLATION_FALLBACKS = '__gedmo.translatable.stored.fallbacks'; /** * Customized object hydrator name * * @internal */ - const HYDRATE_OBJECT_TRANSLATION = '__gedmo.translatable.object.hydrator'; + public const HYDRATE_OBJECT_TRANSLATION = '__gedmo.translatable.object.hydrator'; /** * Customized object hydrator name * * @internal */ - const HYDRATE_SIMPLE_OBJECT_TRANSLATION = '__gedmo.translatable.simple_object.hydrator'; + public const HYDRATE_SIMPLE_OBJECT_TRANSLATION = '__gedmo.translatable.simple_object.hydrator'; /** * Stores all component references from select clause * - * @var array + * @var array> + * + * @phpstan-var array}> */ - private $translatedComponents = array(); + private array $translatedComponents = []; /** * DBAL database platform - * - * @var \Doctrine\DBAL\Platforms\AbstractPlatform */ - private $platform; + private AbstractPlatform $platform; /** * DBAL database connection - * - * @var \Doctrine\DBAL\Connection */ - private $conn; + private Connection $conn; /** * List of aliases to replace with translation * content reference * - * @var array + * @var array */ - private $replacements = array(); + private array $replacements = []; /** * List of joins for translated components in query * - * @var array + * @var array */ - private $components = array(); + private array $components = []; + + private TranslatableListener $listener; - /** - * {@inheritDoc} - */ public function __construct($query, $parserResult, array $queryComponents) { parent::__construct($query, $parserResult, $queryComponents); @@ -99,41 +127,57 @@ public function __construct($query, $parserResult, array $queryComponents) } /** - * {@inheritDoc} + * Gets an executor that can be used to execute the result of this walker. + * + * @param SelectStatement|UpdateStatement|DeleteStatement $statement */ - public function getExecutor($AST) + protected function doGetExecutorWithCompat($statement): AbstractSqlExecutor { - if (!$AST instanceof SelectStatement) { - throw new \Gedmo\Exception\UnexpectedValueException('Translation walker should be used only on select statement'); + // If it's not a Select, the TreeWalker ought to skip it, and just return the parent. + // @see https://github.com/doctrine-extensions/DoctrineExtensions/issues/2013 + if (!$statement instanceof SelectStatement) { + return parent::getExecutor($statement); } $this->prepareTranslatedComponents(); - return new SingleSelectExecutor($AST, $this); + return new SingleSelectExecutor($statement, $this); } /** - * {@inheritDoc} + * @param DeleteStatement|UpdateStatement|SelectStatement $AST */ - public function walkSelectStatement(SelectStatement $AST) + protected function doGetFinalizerWithCompat($AST): SqlFinalizer { - $result = parent::walkSelectStatement($AST); - if (!count($this->translatedComponents)) { + // If it's not a Select, the TreeWalker ought to skip it, and just return the parent. + // @see https://github.com/doctrine-extensions/DoctrineExtensions/issues/2013 + if (!$AST instanceof SelectStatement) { + return parent::getFinalizer($AST); + } + $this->prepareTranslatedComponents(); + + return new SingleSelectSqlFinalizer($this->createSqlForFinalizer($AST)); + } + + protected function createSqlForFinalizer(SelectStatement $selectStatement): string + { + $result = parent::createSqlForFinalizer($selectStatement); + if ([] === $this->translatedComponents) { return $result; } $hydrationMode = $this->getQuery()->getHydrationMode(); - if ($hydrationMode === Query::HYDRATE_OBJECT) { + if (Query::HYDRATE_OBJECT === $hydrationMode) { $this->getQuery()->setHydrationMode(self::HYDRATE_OBJECT_TRANSLATION); $this->getEntityManager()->getConfiguration()->addCustomHydrationMode( self::HYDRATE_OBJECT_TRANSLATION, - 'Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator' + ObjectHydrator::class ); $this->getQuery()->setHint(Query::HINT_REFRESH, true); - } elseif ($hydrationMode === Query::HYDRATE_SIMPLEOBJECT) { + } elseif (Query::HYDRATE_SIMPLEOBJECT === $hydrationMode) { $this->getQuery()->setHydrationMode(self::HYDRATE_SIMPLE_OBJECT_TRANSLATION); $this->getEntityManager()->getConfiguration()->addCustomHydrationMode( self::HYDRATE_SIMPLE_OBJECT_TRANSLATION, - 'Gedmo\\Translatable\\Hydrator\\ORM\\SimpleObjectHydrator' + SimpleObjectHydrator::class ); $this->getQuery()->setHint(Query::HINT_REFRESH, true); } @@ -141,107 +185,53 @@ public function walkSelectStatement(SelectStatement $AST) return $result; } - /** - * {@inheritDoc} - */ - public function walkSelectClause($selectClause) - { - $result = parent::walkSelectClause($selectClause); - $result = $this->replace($this->replacements, $result); - - return $result; - } - - /** - * {@inheritDoc} - */ - public function walkFromClause($fromClause) + protected function doWalkSelectClauseWithCompat(SelectClause $selectClause): string { - $result = parent::walkFromClause($fromClause); - $result .= $this->joinTranslations($fromClause); - - return $result; + return $this->replace($this->replacements, parent::walkSelectClause($selectClause)); } - /** - * {@inheritDoc} - */ - public function walkWhereClause($whereClause) + protected function doWalkFromClauseWithCompat(FromClause $fromClause): string { - $result = parent::walkWhereClause($whereClause); - - return $this->replace($this->replacements, $result); + return parent::walkFromClause($fromClause).$this->joinTranslations($fromClause); } - /** - * {@inheritDoc} - */ - public function walkHavingClause($havingClause) + protected function doWalkWhereClauseWithCompat(?WhereClause $whereClause): string { - $result = parent::walkHavingClause($havingClause); - - return $this->replace($this->replacements, $result); + return $this->replace($this->replacements, parent::walkWhereClause($whereClause)); } - /** - * {@inheritDoc} - */ - public function walkOrderByClause($orderByClause) + protected function doWalkHavingClauseWithCompat(HavingClause $havingClause): string { - $result = parent::walkOrderByClause($orderByClause); - - return $this->replace($this->replacements, $result); + return $this->replace($this->replacements, parent::walkHavingClause($havingClause)); } - /** - * {@inheritDoc} - */ - public function walkSubselect($subselect) + protected function doWalkOrderByClauseWithCompat(OrderByClause $orderByClause): string { - $result = parent::walkSubselect($subselect); - - return $result; + return $this->replace($this->replacements, parent::walkOrderByClause($orderByClause)); } - /** - * {@inheritDoc} - */ - public function walkSubselectFromClause($subselectFromClause) + protected function doWalkSubselectFromClauseWithCompat(SubselectFromClause $subselectFromClause): string { - $result = parent::walkSubselectFromClause($subselectFromClause); - $result .= $this->joinTranslations($subselectFromClause); - - return $result; + return parent::walkSubselectFromClause($subselectFromClause).$this->joinTranslations($subselectFromClause); } - /** - * {@inheritDoc} - */ - public function walkSimpleSelectClause($simpleSelectClause) + protected function doWalkSimpleSelectClauseWithCompat(SimpleSelectClause $simpleSelectClause): string { - $result = parent::walkSimpleSelectClause($simpleSelectClause); - - return $this->replace($this->replacements, $result); + return $this->replace($this->replacements, parent::walkSimpleSelectClause($simpleSelectClause)); } - /** - * {@inheritDoc} - */ - public function walkGroupByClause($groupByClause) + protected function doWalkGroupByClauseWithCompat(GroupByClause $groupByClause): string { - $result = parent::walkGroupByClause($groupByClause); - - return $this->replace($this->replacements, $result); + return $this->replace($this->replacements, parent::walkGroupByClause($groupByClause)); } /** * Walks from clause, and creates translation joins * for the translated components * - * @param \Doctrine\ORM\Query\AST\FromClause $from - * @return string + * @param FromClause|SubselectFromClause $from */ - private function joinTranslations($from) + private function joinTranslations(Node $from): string { $result = ''; foreach ($from->identificationVariableDeclarations as $decl) { @@ -278,9 +268,8 @@ private function joinTranslations($from) * on used query components * * @todo: make it cleaner - * @return string */ - private function prepareTranslatedComponents() + private function prepareTranslatedComponents(): void { $q = $this->getQuery(); $locale = $q->getHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE); @@ -289,62 +278,66 @@ private function prepareTranslatedComponents() $locale = $this->listener->getListenerLocale(); } $defaultLocale = $this->listener->getDefaultLocale(); - if ($locale === $defaultLocale && !$this->listener->getPersistDefaultLocaleTranslation()) { + if ($locale === $defaultLocale && !$this->listener->getPersistDefaultLocaleTranslation()) { // Skip preparation as there's no need to translate anything return; } $em = $this->getEntityManager(); $ea = new TranslatableEventAdapter(); $ea->setEntityManager($em); + $quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); $joinStrategy = $q->getHint(TranslatableListener::HINT_INNER_JOIN) ? 'INNER' : 'LEFT'; foreach ($this->translatedComponents as $dqlAlias => $comp) { + /** @var ClassMetadata $meta */ $meta = $comp['metadata']; - $config = $this->listener->getConfiguration($em, $meta->name); - $transClass = $this->listener->getTranslationClass($ea, $meta->name); + $config = $this->listener->getConfiguration($em, $meta->getName()); + $transClass = $this->listener->getTranslationClass($ea, $meta->getName()); $transMeta = $em->getClassMetadata($transClass); - $transTable = $em->getConfiguration()->getQuoteStrategy()->getTableName($transMeta, $this->platform); + $transTable = $quoteStrategy->getTableName($transMeta, $this->platform); foreach ($config['fields'] as $field) { $compTblAlias = $this->walkIdentificationVariable($dqlAlias, $field); $tblAlias = $this->getSQLTableAlias('trans'.$compTblAlias.$field); $sql = " {$joinStrategy} JOIN ".$transTable.' '.$tblAlias; - $sql .= ' ON '.$tblAlias.'.'.$transMeta->getQuotedColumnName('locale', $this->platform) + $sql .= ' ON '.$tblAlias.'.'.$quoteStrategy->getColumnName('locale', $transMeta, $this->platform) .' = '.$this->conn->quote($locale); - $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('field', $this->platform) + $sql .= ' AND '.$tblAlias.'.'.$quoteStrategy->getColumnName('field', $transMeta, $this->platform) .' = '.$this->conn->quote($field); $identifier = $meta->getSingleIdentifierFieldName(); - $idColName = $meta->getQuotedColumnName($identifier, $this->platform); + $idColName = $quoteStrategy->getColumnName($identifier, $meta, $this->platform); if ($ea->usesPersonalTranslation($transClass)) { $sql .= ' AND '.$tblAlias.'.'.$transMeta->getSingleAssociationJoinColumnName('object') .' = '.$compTblAlias.'.'.$idColName; } else { - $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('objectClass', $this->platform) + $sql .= ' AND '.$tblAlias.'.'.$quoteStrategy->getColumnName('objectClass', $transMeta, $this->platform) .' = '.$this->conn->quote($config['useObjectClass']); $mappingFK = $transMeta->getFieldMapping('foreignKey'); $mappingPK = $meta->getFieldMapping($identifier); - $fkColName = $this->getCastedForeignKey($compTblAlias.'.'.$idColName, $mappingFK['type'], $mappingPK['type']); - $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('foreignKey', $this->platform) + $fkColName = $this->getCastedForeignKey($compTblAlias.'.'.$idColName, $mappingFK->type ?? $mappingFK['type'], $mappingPK->type ?? $mappingPK['type']); + $sql .= ' AND '.$tblAlias.'.'.$quoteStrategy->getColumnName('foreignKey', $transMeta, $this->platform) .' = '.$fkColName; } isset($this->components[$dqlAlias]) ? $this->components[$dqlAlias] .= $sql : $this->components[$dqlAlias] = $sql; - $originalField = $compTblAlias.'.'.$meta->getQuotedColumnName($field, $this->platform); - $substituteField = $tblAlias.'.'.$transMeta->getQuotedColumnName('content', $this->platform); + $originalField = $compTblAlias.'.'.$quoteStrategy->getColumnName($field, $meta, $this->platform); + $substituteField = $tblAlias.'.'.$quoteStrategy->getColumnName('content', $transMeta, $this->platform); // Treat translation as original field type $fieldMapping = $meta->getFieldMapping($field); - if ((($this->platform instanceof MySqlPlatform) && - in_array($fieldMapping["type"], array("decimal"))) || - (!($this->platform instanceof MySqlPlatform) && - !in_array($fieldMapping["type"], array("datetime", "datetimetz", "date", "time")))) { - $type = Type::getType($fieldMapping["type"]); - $substituteField = 'CAST('.$substituteField.' AS '.$type->getSQLDeclaration($fieldMapping, $this->platform).')'; + if ((($this->platform instanceof AbstractMySQLPlatform) + && in_array($fieldMapping->type ?? $fieldMapping['type'], ['decimal'], true)) + || (!($this->platform instanceof AbstractMySQLPlatform) + && !in_array($fieldMapping->type ?? $fieldMapping['type'], ['datetime', 'datetimetz', 'date', 'time'], true))) { + $type = Type::getType($fieldMapping->type ?? $fieldMapping['type']); + + // In ORM 2.x, $fieldMapping is an array. In ORM 3.x, it's a data object. Always cast to an array for compatibility across versions. + $substituteField = 'CAST('.$substituteField.' AS '.$type->getSQLDeclaration((array) $fieldMapping, $this->platform).')'; } // Fallback to original if was asked for if (($this->needsFallback() && (!isset($config['fallback'][$field]) || $config['fallback'][$field])) - || (!$this->needsFallback() && isset($config['fallback'][$field]) && $config['fallback'][$field]) + || (!$this->needsFallback() && isset($config['fallback'][$field]) && $config['fallback'][$field]) ) { $substituteField = 'COALESCE('.$substituteField.', '.$originalField.')'; } @@ -356,10 +349,8 @@ private function prepareTranslatedComponents() /** * Checks if translation fallbacks are needed - * - * @return boolean */ - private function needsFallback() + private function needsFallback(): bool { $q = $this->getQuery(); $fallback = $q->getHint(TranslatableListener::HINT_FALLBACK); @@ -375,9 +366,11 @@ private function needsFallback() /** * Search for translated components in the select clause * - * @param array $queryComponents + * @param array>> $queryComponents + * + * @phpstan-param array}> $queryComponents */ - private function extractTranslatedComponents(array $queryComponents) + private function extractTranslatedComponents(array $queryComponents): void { $em = $this->getEntityManager(); foreach ($queryComponents as $alias => $comp) { @@ -385,7 +378,7 @@ private function extractTranslatedComponents(array $queryComponents) continue; } $meta = $comp['metadata']; - $config = $this->listener->getConfiguration($em, $meta->name); + $config = $this->listener->getConfiguration($em, $meta->getName()); if ($config && isset($config['fields'])) { $this->translatedComponents[$alias] = $comp; } @@ -395,39 +388,32 @@ private function extractTranslatedComponents(array $queryComponents) /** * Get the currently used TranslatableListener * - * @throws \Gedmo\Exception\RuntimeException - if listener is not found - * - * @return TranslatableListener + * @throws RuntimeException if listener is not found */ - private function getTranslatableListener() + private function getTranslatableListener(): TranslatableListener { $em = $this->getEntityManager(); - foreach ($em->getEventManager()->getListeners() as $event => $listeners) { - foreach ($listeners as $hash => $listener) { + foreach ($em->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { if ($listener instanceof TranslatableListener) { return $listener; } } } - throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found'); + throw new RuntimeException('The translation listener could not be found'); } /** * Replaces given sql $str with required * results * - * @param array $repl - * @param string $str - * - * @return string + * @param array $repl */ - private function replace(array $repl, $str) + private function replace(array $repl, string $str): string { foreach ($repl as $target => $result) { - $str = preg_replace_callback('/(\s|\()('.$target.')(,?)(\s|\))/smi', function ($m) use ($result) { - return $m[1].$result.$m[3].$m[4]; - }, $str); + $str = preg_replace_callback('/(\s|\()('.$target.')(,?)(\s|\)|$)/smi', static fn (array $m): string => $m[1].$result.$m[3].$m[4], $str); } return $str; @@ -435,14 +421,16 @@ private function replace(array $repl, $str) /** * Casts a foreign key if needed + * * @NOTE: personal translations manages that for themselves. * - * @param $component - a column with an alias to cast - * @param $typeFK - translation table foreign key type - * @param $typePK - primary key type which references translation table - * @return string - modified $component if needed + * @param string $component a column with an alias to cast + * @param string $typeFK translation table foreign key type + * @param string $typePK primary key type which references translation table + * + * @return string modified $component if needed */ - private function getCastedForeignKey($component, $typeFK, $typePK) + private function getCastedForeignKey(string $component, string $typeFK, string $typePK): string { // the keys are of same type if ($typeFK === $typePK) { @@ -450,13 +438,14 @@ private function getCastedForeignKey($component, $typeFK, $typePK) } // try to look at postgres casting - if ($this->platform instanceof PostgreSqlPlatform) { + if ($this->platform instanceof PostgreSQLPlatform) { switch ($typeFK) { - case 'string': - case 'guid': - // need to cast to VARCHAR - $component = $component . '::VARCHAR'; - break; + case 'string': + case 'guid': + // need to cast to VARCHAR + $component .= '::VARCHAR'; + + break; } } diff --git a/lib/Gedmo/Translatable/Translatable.php b/src/Translatable/Translatable.php similarity index 57% rename from lib/Gedmo/Translatable/Translatable.php rename to src/Translatable/Translatable.php index 44fabaf699..4f3103d13a 100644 --- a/lib/Gedmo/Translatable/Translatable.php +++ b/src/Translatable/Translatable.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable; /** @@ -8,26 +15,25 @@ * Translatable * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ interface Translatable { // use now annotations instead of predefined methods, this interface is not necessary - /** - * @gedmo:TranslationEntity + /* + * @Gedmo\TranslationEntity * to specify custom translation class use - * class annotation @gedmo:TranslationEntity(class="your\class") + * class annotation @Gedmo\TranslationEntity(class="your\class") */ - /** - * @gedmo:Translatable + /* + * @Gedmo\Translatable * to mark the field as translatable, * these fields will be translated */ - /** - * @gedmo:Locale OR @gedmo:Language + /* + * @Gedmo\Locale OR @Gedmo\Language * to mark the field as locale used to override global * locale settings from TranslatableListener */ diff --git a/lib/Gedmo/Translatable/TranslatableListener.php b/src/Translatable/TranslatableListener.php similarity index 74% rename from lib/Gedmo/Translatable/TranslatableListener.php rename to src/Translatable/TranslatableListener.php index 4eec1f9533..ad8e22f477 100644 --- a/lib/Gedmo/Translatable/TranslatableListener.php +++ b/src/Translatable/TranslatableListener.php @@ -1,12 +1,26 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translatable; use Doctrine\Common\EventArgs; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ORM\ORMInvalidArgumentException; -use Gedmo\Tool\Wrapper\AbstractWrapper; +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Event\ManagerEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Exception\RuntimeException; use Gedmo\Mapping\MappedEventSubscriber; +use Gedmo\Tool\Wrapper\AbstractWrapper; use Gedmo\Translatable\Mapping\Event\TranslatableAdapter; /** @@ -22,7 +36,18 @@ * the caching is activated for metadata * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @phpstan-type TranslatableConfiguration = array{ + * fields?: string[], + * fallback?: array, + * locale?: string, + * translationClass?: class-string, + * useObjectClass?: class-string, + * } + * + * @phpstan-extends MappedEventSubscriber + * + * @final since gedmo/doctrine-extensions 3.11 */ class TranslatableListener extends MappedEventSubscriber { @@ -30,17 +55,17 @@ class TranslatableListener extends MappedEventSubscriber * Query hint to override the fallback of translations * integer 1 for true, 0 false */ - const HINT_FALLBACK = 'gedmo.translatable.fallback'; + public const HINT_FALLBACK = 'gedmo.translatable.fallback'; /** * Query hint to override the fallback locale */ - const HINT_TRANSLATABLE_LOCALE = 'gedmo.translatable.locale'; + public const HINT_TRANSLATABLE_LOCALE = 'gedmo.translatable.locale'; /** * Query hint to use inner join strategy for translations */ - const HINT_INNER_JOIN = 'gedmo.translatable.inner_join.translations'; + public const HINT_INNER_JOIN = 'gedmo.translatable.inner_join.translations'; /** * Locale which is set on this listener. @@ -57,80 +82,77 @@ class TranslatableListener extends MappedEventSubscriber * which is used for updating is not default. This * will load the default translation in other locales * if record is not translated yet - * - * @var string */ - private $defaultLocale = 'en_US'; + private string $defaultLocale = 'en_US'; /** * If this is set to false, when if entity does * not have a translation for requested locale * it will show a blank value - * - * @var boolean */ - private $translationFallback = false; + private bool $translationFallback = false; /** * List of translations which do not have the foreign * key generated yet - MySQL case. These translations * will be updated with new keys on postPersist event * - * @var array + * @var array> */ - private $pendingTranslationInserts = array(); + private array $pendingTranslationInserts = []; /** * Currently in case if there is TranslationQueryWalker * in charge. We need to skip issuing additional queries * on load - * - * @var boolean */ - private $skipOnLoad = false; + private bool $skipOnLoad = false; /** * Tracks locale the objects currently translated in * - * @var array + * @var array */ - private $translatedInLocale = array(); + private array $translatedInLocale = []; /** * Whether or not, to persist default locale * translation or keep it in original record - * - * @var boolean */ - private $persistDefaultLocaleTranslation = false; + private bool $persistDefaultLocaleTranslation = false; /** * Tracks translation object for default locale * - * @var array + * @var array> */ - private $translationInDefaultLocale = array(); + private array $translationInDefaultLocale = []; + + /** + * Default translation value upon missing translation + */ + private ?string $defaultTranslationValue = null; /** * Specifies the list of events to listen * - * @return array + * @return string[] */ public function getSubscribedEvents() { - return array( + return [ 'postLoad', 'postPersist', 'preFlush', 'onFlush', 'loadClassMetadata', - ); + ]; } /** * Set to skip or not onLoad event * - * @param boolean $bool + * @param bool $bool * * @return static */ @@ -145,7 +167,7 @@ public function setSkipOnLoad($bool) * Whether or not, to persist default locale * translation or keep it in original record * - * @param boolean $bool + * @param bool $bool * * @return static */ @@ -160,7 +182,7 @@ public function setPersistDefaultLocaleTranslation($bool) * Check if should persist default locale * translation or keep it in original record * - * @return boolean + * @return bool */ public function getPersistDefaultLocaleTranslation() { @@ -171,8 +193,10 @@ public function getPersistDefaultLocaleTranslation() * Add additional $translation for pending $oid object * which is being inserted * - * @param string $oid + * @param int $oid * @param object $translation + * + * @return void */ public function addPendingTranslationInsert($oid, $translation) { @@ -182,28 +206,32 @@ public function addPendingTranslationInsert($oid, $translation) /** * Maps additional metadata * - * @param EventArgs $eventArgs + * @param LoadClassMetadataEventArgs $eventArgs + * + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $eventArgs + * + * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { - $ea = $this->getEventAdapter($eventArgs); - $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata()); + $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } /** * Get the translation class to be used * for the object $class * - * @param TranslatableAdapter $ea - * @param string $class + * @param string $class + * + * @phpstan-param class-string $class * * @return string + * + * @phpstan-return class-string */ public function getTranslationClass(TranslatableAdapter $ea, $class) { - return isset(self::$configurations[$this->name][$class]['translationClass']) ? - self::$configurations[$this->name][$class]['translationClass'] : - $ea->getDefaultTranslationClass() + return self::$configurations[$this->name][$class]['translationClass'] ?? $ea->getDefaultTranslationClass() ; } @@ -211,7 +239,7 @@ public function getTranslationClass(TranslatableAdapter $ea, $class) * Enable or disable translation fallback * to original record value * - * @param boolean $bool + * @param bool $bool * * @return static */ @@ -226,7 +254,7 @@ public function setTranslationFallback($bool) * Weather or not is using the translation * fallback to original record * - * @return boolean + * @return bool */ public function getTranslationFallback() { @@ -248,6 +276,17 @@ public function setTranslatableLocale($locale) return $this; } + /** + * Set the default translation value on missing translation + * + * @deprecated usage of a non nullable value for defaultTranslationValue is deprecated + * and will be removed on the next major release which will rely on the expected types + */ + public function setDefaultTranslationValue(?string $defaultTranslationValue): void + { + $this->defaultTranslationValue = $defaultTranslationValue; + } + /** * Sets the default locale, this changes behavior * to not update the original record field if locale @@ -288,38 +327,41 @@ public function getListenerLocale() /** * Gets the locale to use for translation. Loads object - * defined locale first.. + * defined locale first. * - * @param object $object - * @param object $meta - * @param object $om + * @param object $object + * @param ClassMetadata $meta + * @param object $om + * + * @throws RuntimeException if language or locale property is not found in entity * - * @throws \Gedmo\Exception\RuntimeException - if language or locale property is not - * found in entity * @return string */ public function getTranslatableLocale($object, $meta, $om = null) { $locale = $this->locale; - if (isset(self::$configurations[$this->name][$meta->name]['locale'])) { - /** @var \ReflectionClass $class */ + $configurationLocale = self::$configurations[$this->name][$meta->getName()]['locale'] ?? null; + if (null !== $configurationLocale) { $class = $meta->getReflectionClass(); - $reflectionProperty = $class->getProperty(self::$configurations[$this->name][$meta->name]['locale']); - if (!$reflectionProperty) { - $column = self::$configurations[$this->name][$meta->name]['locale']; - throw new \Gedmo\Exception\RuntimeException("There is no locale or language property ({$column}) found on object: {$meta->name}"); + if (!$class->hasProperty($configurationLocale)) { + throw new RuntimeException("There is no locale or language property ({$configurationLocale}) found on object: {$meta->getName()}"); + } + $reflectionProperty = $class->getProperty($configurationLocale); + + if (PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(true); } - $reflectionProperty->setAccessible(true); + $value = $reflectionProperty->getValue($object); if (is_object($value) && method_exists($value, '__toString')) { - $value = (string) $value; + $value = $value->__toString(); } if ($this->isValidLocale($value)) { $locale = $value; } } elseif ($om instanceof DocumentManager) { - list($mapping, $parentObject) = $om->getUnitOfWork()->getParentAssociation($object); - if ($parentObject != null) { + [, $parentObject] = $om->getUnitOfWork()->getParentAssociation($object); + if (null !== $parentObject) { $parentMeta = $om->getClassMetadata(get_class($parentObject)); $locale = $this->getTranslatableLocale($parentObject, $parentMeta, $om); } @@ -334,7 +376,11 @@ public function getTranslatableLocale($object, $meta, $om = null) * This has to be done in the preFlush because, when an entity has been loaded * in a different locale, no changes will be detected. * - * @param EventArgs $args + * @param ManagerEventArgs $args + * + * @phpstan-param ManagerEventArgs $args + * + * @return void */ public function preFlush(EventArgs $args) { @@ -344,6 +390,9 @@ public function preFlush(EventArgs $args) foreach ($this->translationInDefaultLocale as $oid => $fields) { $trans = reset($fields); + + assert(false !== $trans); + if ($ea->usesPersonalTranslation(get_class($trans))) { $entity = $trans->getObject(); } else { @@ -368,7 +417,11 @@ public function preFlush(EventArgs $args) * Looks for translatable objects being inserted or updated * for further processing * - * @param EventArgs $args + * @param ManagerEventArgs $args + * + * @phpstan-param ManagerEventArgs $args + * + * @return void */ public function onFlush(EventArgs $args) { @@ -378,7 +431,7 @@ public function onFlush(EventArgs $args) // check all scheduled inserts for Translatable objects foreach ($ea->getScheduledObjectInsertions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - $config = $this->getConfiguration($om, $meta->name); + $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['fields'])) { $this->handleTranslatableObjectUpdate($ea, $object, true); } @@ -386,7 +439,7 @@ public function onFlush(EventArgs $args) // check all scheduled updates for Translatable entities foreach ($ea->getScheduledObjectUpdates($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - $config = $this->getConfiguration($om, $meta->name); + $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['fields'])) { $this->handleTranslatableObjectUpdate($ea, $object, false); } @@ -394,20 +447,25 @@ public function onFlush(EventArgs $args) // check scheduled deletions for Translatable entities foreach ($ea->getScheduledObjectDeletions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - $config = $this->getConfiguration($om, $meta->name); + $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['fields'])) { $wrapped = AbstractWrapper::wrap($object, $om); - $transClass = $this->getTranslationClass($ea, $meta->name); + $transClass = $this->getTranslationClass($ea, $meta->getName()); + \assert($wrapped instanceof AbstractWrapper); $ea->removeAssociatedTranslations($wrapped, $transClass, $config['useObjectClass']); } } } - /** + /** * Checks for inserted object to update their translation * foreign keys * - * @param EventArgs $args + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void */ public function postPersist(EventArgs $args) { @@ -416,8 +474,8 @@ public function postPersist(EventArgs $args) $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); // check if entity is tracked by translatable and without foreign key - if ($this->getConfiguration($om, $meta->name) && count($this->pendingTranslationInserts)) { - $oid = spl_object_hash($object); + if ($this->getConfiguration($om, $meta->getName()) && [] !== $this->pendingTranslationInserts) { + $oid = spl_object_id($object); if (array_key_exists($oid, $this->pendingTranslationInserts)) { // load the pending translations without key $wrapped = AbstractWrapper::wrap($object, $om); @@ -440,7 +498,11 @@ public function postPersist(EventArgs $args) * After object is loaded, listener updates the translations * by currently used locale * - * @param EventArgs $args + * @param ManagerEventArgs $args + * + * @phpstan-param ManagerEventArgs $args + * + * @return void */ public function postLoad(EventArgs $args) { @@ -448,10 +510,12 @@ public function postLoad(EventArgs $args) $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - $config = $this->getConfiguration($om, $meta->name); + $config = $this->getConfiguration($om, $meta->getName()); + $locale = $this->defaultLocale; + $oid = null; if (isset($config['fields'])) { $locale = $this->getTranslatableLocale($object, $meta, $om); - $oid = spl_object_hash($object); + $oid = spl_object_id($object); $this->translatedInLocale[$oid] = $locale; } @@ -470,15 +534,18 @@ public function postLoad(EventArgs $args) ); // translate object's translatable properties foreach ($config['fields'] as $field) { - $translated = ''; - foreach ((array) $result as $entry) { + $translated = $this->defaultTranslationValue; + + foreach ($result as $entry) { if ($entry['field'] == $field) { - $translated = $entry['content']; + $translated = $entry['content'] ?? null; + break; } } + // update translation - if ($translated + if ($this->defaultTranslationValue !== $translated || (!$this->translationFallback && (!isset($config['fallback'][$field]) || !$config['fallback'][$field])) || ($this->translationFallback && isset($config['fallback'][$field]) && !$config['fallback'][$field]) ) { @@ -486,9 +553,9 @@ public function postLoad(EventArgs $args) // ensure clean changeset $ea->setOriginalObjectProperty( $om->getUnitOfWork(), - $oid, + $object, $field, - $meta->getReflectionProperty($field)->getValue($object) + $meta->getFieldValue($object, $field) ); } } @@ -496,8 +563,43 @@ public function postLoad(EventArgs $args) } /** - * {@inheritDoc} + * Sets translation object which represents translation in default language. + * + * @param int $oid hash of basic entity + * @param string $field field of basic entity + * @param object|Translatable $trans Translation object + * + * @return void + */ + public function setTranslationInDefaultLocale($oid, $field, $trans) + { + if (!isset($this->translationInDefaultLocale[$oid])) { + $this->translationInDefaultLocale[$oid] = []; + } + $this->translationInDefaultLocale[$oid][$field] = $trans; + } + + /** + * @return bool + */ + public function isSkipOnLoad() + { + return $this->skipOnLoad; + } + + /** + * Check if object has any translation object which represents translation in default language. + * This is for internal use only. + * + * @param int $oid hash of the basic entity + * + * @return bool */ + public function hasTranslationsInDefaultLocale($oid) + { + return array_key_exists($oid, $this->translationInDefaultLocale); + } + protected function getNamespace() { return __NAMESPACE__; @@ -506,25 +608,23 @@ protected function getNamespace() /** * Validates the given locale * - * @param string $locale - locale to validate + * @param string $locale locale to validate + * + * @throws InvalidArgumentException if locale is not valid * - * @throws \Gedmo\Exception\InvalidArgumentException if locale is not valid + * @return void */ protected function validateLocale($locale) { if (!$this->isValidLocale($locale)) { - throw new \Gedmo\Exception\InvalidArgumentException('Locale or language cannot be empty and must be set through Listener or Entity'); + throw new InvalidArgumentException('Locale or language cannot be empty and must be set through Listener or Entity'); } } - + /** * Check if the given locale is valid - * - * @param string $locale - locale to check - * - * @return bool */ - private function isValidlocale($locale) + private function isValidLocale(?string $locale): bool { return is_string($locale) && strlen($locale); } @@ -532,19 +632,15 @@ private function isValidlocale($locale) /** * Creates the translation for object being flushed * - * @param TranslatableAdapter $ea - * @param object $object - * @param boolean $isInsert - * - * @throws \UnexpectedValueException - if locale is not valid, or + * @throws \UnexpectedValueException if locale is not valid, or * primary key is composite, missing or invalid */ - private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, $object, $isInsert) + private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, object $object, bool $isInsert): void { $om = $ea->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $om); $meta = $wrapped->getMetadata(); - $config = $this->getConfiguration($om, $meta->name); + $config = $this->getConfiguration($om, $meta->getName()); // no need cache, metadata is loaded only once in MetadataFactoryClass $translationClass = $this->getTranslationClass($ea, $config['useObjectClass']); $translationMetadata = $om->getClassMetadata($translationClass); @@ -555,7 +651,7 @@ private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, $object $locale = $this->getTranslatableLocale($object, $meta, $om); $uow = $om->getUnitOfWork(); - $oid = spl_object_hash($object); + $oid = spl_object_id($object); $changeSet = $ea->getObjectChangeSet($uow, $object); $translatableFields = $config['fields']; foreach ($translatableFields as $field) { @@ -573,6 +669,7 @@ private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, $object && $trans->getField() === $field && $this->belongsToObject($ea, $trans, $object)) { $this->setTranslationInDefaultLocale($oid, $field, $trans); + break; } } @@ -594,12 +691,15 @@ private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, $object if ($wasPersistedSeparetely) { $translation = $trans; + break; } } // check if translation already is created if (!$isInsert && !$translation) { + \assert($wrapped instanceof AbstractWrapper); + $translation = $ea->findTranslation( $wrapped, $locale, @@ -631,11 +731,11 @@ private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, $object $translation->setContent($content); // check if need to update in database $transWrapper = AbstractWrapper::wrap($translation, $om); - if (((is_null($content) && !$isInsert) || is_bool($content) || is_int($content) || (is_string($content) && strlen($content) > 0) || !empty($content)) && ($isInsert || !$transWrapper->getIdentifier() || isset($changeSet[$field]))) { + if (((null === $content && !$isInsert) || is_bool($content) || is_int($content) || is_string($content) || !empty($content)) && ($isInsert || !$transWrapper->getIdentifier() || isset($changeSet[$field]))) { if ($isInsert && !$objectId && !$ea->usesPersonalTranslation($translationClass)) { // if we do not have the primary key yet available // keep this translation in memory to insert it later with foreign key - $this->pendingTranslationInserts[spl_object_hash($object)][] = $translation; + $this->pendingTranslationInserts[spl_object_id($object)][] = $translation; } else { // persist and compute change set for translation if ($wasPersistedSeparetely) { @@ -648,7 +748,7 @@ private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, $object } } - if ($isInsert && $this->getTranslationInDefaultLocale($oid, $field) !== null) { + if ($isInsert && null !== $this->getTranslationInDefaultLocale($oid, $field)) { // We can't rely on object field value which is created in non-default locale. // If we provide translation for default locale as well, the latter is considered to be trusted // and object content should be overridden. @@ -663,9 +763,9 @@ private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, $object $this->validateLocale($this->defaultLocale); $modifiedChangeSet = $changeSet; foreach ($changeSet as $field => $changes) { - if (in_array($field, $translatableFields)) { + if (in_array($field, $translatableFields, true)) { if ($locale !== $this->defaultLocale) { - $ea->setOriginalObjectProperty($uow, $oid, $field, $changes[0]); + $ea->setOriginalObjectProperty($uow, $object, $field, $changes[0]); unset($modifiedChangeSet[$field]); } } @@ -673,14 +773,14 @@ private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, $object $ea->recomputeSingleObjectChangeset($uow, $meta, $object); // cleanup current changeset only if working in a another locale different than de default one, otherwise the changeset will always be reverted if ($locale !== $this->defaultLocale) { - $ea->clearObjectChangeSet($uow, $oid); + $ea->clearObjectChangeSet($uow, $object); // recompute changeset only if there are changes other than reverted translations if ($modifiedChangeSet || $this->hasTranslationsInDefaultLocale($oid)) { foreach ($modifiedChangeSet as $field => $changes) { - $ea->setOriginalObjectProperty($uow, $oid, $field, $changes[0]); + $ea->setOriginalObjectProperty($uow, $object, $field, $changes[0]); } foreach ($translatableFields as $field) { - if ($this->getTranslationInDefaultLocale($oid, $field) !== null) { + if (null !== $this->getTranslationInDefaultLocale($oid, $field)) { $wrapped->setPropertyValue($field, $this->getTranslationInDefaultLocale($oid, $field)->getContent()); $this->removeTranslationInDefaultLocale($oid, $field); } @@ -691,43 +791,20 @@ private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, $object } } - /** - * Sets translation object which represents translation in default language. - * - * @param string $oid hash of basic entity - * @param string $field field of basic entity - * @param mixed $trans Translation object - */ - public function setTranslationInDefaultLocale($oid, $field, $trans) - { - if (!isset($this->translationInDefaultLocale[$oid])) { - $this->translationInDefaultLocale[$oid] = array(); - } - $this->translationInDefaultLocale[$oid][$field] = $trans; - } - - /** - * @return boolean - */ - public function isSkipOnLoad() - { - return $this->skipOnLoad; - } - /** * Removes translation object which represents translation in default language. * This is for internal use only. * - * @param string $oid hash of the basic entity + * @param int $oid hash of the basic entity * @param string $field field of basic entity */ - private function removeTranslationInDefaultLocale($oid, $field) + private function removeTranslationInDefaultLocale(int $oid, string $field): void { if (isset($this->translationInDefaultLocale[$oid])) { if (isset($this->translationInDefaultLocale[$oid][$field])) { unset($this->translationInDefaultLocale[$oid][$field]); } - if (! $this->translationInDefaultLocale[$oid]) { + if (!$this->translationInDefaultLocale[$oid]) { // We removed the final remaining elements from the // translationInDefaultLocale[$oid] array, so we might as well // completely remove the entry at $oid. @@ -740,55 +817,26 @@ private function removeTranslationInDefaultLocale($oid, $field) * Gets translation object which represents translation in default language. * This is for internal use only. * - * @param string $oid hash of the basic entity + * @param int $oid hash of the basic entity * @param string $field field of basic entity * - * @return mixed Returns translation object if it exists or NULL otherwise - */ - private function getTranslationInDefaultLocale($oid, $field) - { - if (array_key_exists($oid, $this->translationInDefaultLocale)) { - if (array_key_exists($field, $this->translationInDefaultLocale[$oid])) { - $ret = $this->translationInDefaultLocale[$oid][$field]; - } else { - $ret = null; - } - } else { - $ret = null; - } - - return $ret; - } - - /** - * Check if object has any translation object which represents translation in default language. - * This is for internal use only. - * - * @param string $oid hash of the basic entity - * - * @return bool + * @return object|Translatable|null Returns translation object if it exists or NULL otherwise */ - public function hasTranslationsInDefaultLocale($oid) + private function getTranslationInDefaultLocale(int $oid, string $field) { - return array_key_exists($oid, $this->translationInDefaultLocale); + return $this->translationInDefaultLocale[$oid][$field] ?? null; } /** * Checks if the translation entity belongs to the object in question - * - * @param TranslatableAdapter $ea - * @param object $trans - * @param object $object - * - * @return boolean */ - private function belongsToObject(TranslatableAdapter $ea, $trans, $object) + private function belongsToObject(TranslatableAdapter $ea, object $trans, object $object): bool { if ($ea->usesPersonalTranslation(get_class($trans))) { return $trans->getObject() === $object; } - return ($trans->getForeignKey() === $object->getId() - && ($trans->getObjectClass() === get_class($object))); + return $trans->getForeignKey() === $object->getId() + && ($trans->getObjectClass() === get_class($object)); } } diff --git a/src/Translator/Document/Translation.php b/src/Translator/Document/Translation.php new file mode 100644 index 0000000000..285c15917b --- /dev/null +++ b/src/Translator/Document/Translation.php @@ -0,0 +1,65 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Translator\Document; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; +use Gedmo\Translator\Translation as BaseTranslation; + +/** + * Document translation class. + * + * @author Konstantin Kudryashov + * + * @ODM\MappedSuperclass + */ +#[ODM\MappedSuperclass] +abstract class Translation extends BaseTranslation +{ + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] + protected $id; + + /** + * @var string|null + * + * @ODM\Field(type="string") + */ + #[ODM\Field(type: Type::STRING)] + protected $locale; + + /** + * @var string|null + * + * @ODM\Field(type="string") + */ + #[ODM\Field(type: Type::STRING)] + protected $property; + + /** + * @var string|null + * + * @ODM\Field(type="string") + */ + #[ODM\Field(type: Type::STRING)] + protected $value; + + /** + * @return string|null $id + */ + public function getId() + { + return $this->id; + } +} diff --git a/src/Translator/Entity/Translation.php b/src/Translator/Entity/Translation.php new file mode 100644 index 0000000000..52a3c8190a --- /dev/null +++ b/src/Translator/Entity/Translation.php @@ -0,0 +1,71 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Translator\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Translator\Translation as BaseTranslation; + +/** + * Entity translation class. + * + * @author Konstantin Kudryashov + * + * @ORM\MappedSuperclass + */ +#[ORM\MappedSuperclass] +abstract class Translation extends BaseTranslation +{ + /** + * @var int + * + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue + */ + #[ORM\Column(type: Types::INTEGER)] + #[ORM\Id] + #[ORM\GeneratedValue] + protected $id; + + /** + * @var string + * + * @ORM\Column(type="string", length=8) + */ + #[ORM\Column(type: Types::STRING, length: 8)] + protected $locale; + + /** + * @var string + * + * @ORM\Column(type="string", length=32) + */ + #[ORM\Column(type: Types::STRING, length: 32)] + protected $property; + + /** + * @var string + * + * @ORM\Column(type="text", nullable=true) + */ + #[ORM\Column(type: Types::TEXT, nullable: true)] + protected $value; + + /** + * Get id + * + * @return int $id + */ + public function getId() + { + return $this->id; + } +} diff --git a/lib/Gedmo/Translator/Translation.php b/src/Translator/Translation.php similarity index 68% rename from lib/Gedmo/Translator/Translation.php rename to src/Translator/Translation.php index e30e96d1fb..d27c7221fb 100644 --- a/lib/Gedmo/Translator/Translation.php +++ b/src/Translator/Translation.php @@ -1,24 +1,45 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translator; /** * Base translation class. * - * @author Konstantin Kudryashov - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * @author Konstantin Kudryashov */ abstract class Translation implements TranslationInterface { + /** + * @var object|null + */ protected $translatable; + + /** + * @var string|null + */ protected $locale; + + /** + * @var string|null + */ protected $property; + + /** + * @var string|null + */ protected $value; /** * Set translatable * - * @param string $translatable + * @param object $translatable */ public function setTranslatable($translatable) { @@ -28,7 +49,7 @@ public function setTranslatable($translatable) /** * Get translatable * - * @return string + * @return object|null */ public function getTranslatable() { @@ -48,7 +69,7 @@ public function setLocale($locale) /** * Get locale * - * @return string + * @return string|null */ public function getLocale() { @@ -68,7 +89,7 @@ public function setProperty($property) /** * Get property * - * @return string + * @return string|null */ public function getProperty() { @@ -92,7 +113,7 @@ public function setValue($value) /** * Get value * - * @return string + * @return string|null */ public function getValue() { diff --git a/src/Translator/TranslationInterface.php b/src/Translator/TranslationInterface.php new file mode 100644 index 0000000000..b46d9234ac --- /dev/null +++ b/src/Translator/TranslationInterface.php @@ -0,0 +1,82 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Translator; + +/** + * Object for managing translations. + * + * @author Konstantin Kudryashov + */ +interface TranslationInterface +{ + /** + * Set the translatable item. + * + * @param object $translatable + * + * @return void + */ + public function setTranslatable($translatable); + + /** + * Get the translatable item. + * + * @return object + */ + public function getTranslatable(); + + /** + * Set the translation locale. + * + * @param string $locale + * + * @return void + */ + public function setLocale($locale); + + /** + * Get the translation locale. + * + * @return string + */ + public function getLocale(); + + /** + * Set the translated property. + * + * @param string $property + * + * @return void + */ + public function setProperty($property); + + /** + * Get the translated property. + * + * @return string + */ + public function getProperty(); + + /** + * Set the translation value. + * + * @param string $value + * + * @return static + */ + public function setValue($value); + + /** + * Get the translation value. + * + * @return string + */ + public function getValue(); +} diff --git a/lib/Gedmo/Translator/TranslationProxy.php b/src/Translator/TranslationProxy.php similarity index 58% rename from lib/Gedmo/Translator/TranslationProxy.php rename to src/Translator/TranslationProxy.php index 6ae736372a..937f5a1dc6 100644 --- a/lib/Gedmo/Translator/TranslationProxy.php +++ b/src/Translator/TranslationProxy.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Translator; use Doctrine\Common\Collections\Collection; @@ -7,55 +14,72 @@ /** * Proxy class for Entity/Document translations. * - * @author Konstantin Kudryashov - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * @author Konstantin Kudryashov */ class TranslationProxy { + /** + * @var string + */ protected $locale; + /** + * @var object + */ protected $translatable; - protected $properties = array(); + /** + * @var string[] + */ + protected $properties = []; + /** + * @var string + * + * @phpstan-var class-string + */ protected $class; /** - * @var Collection|TranslationInterface[] + * @var Collection */ protected $coll; /** * Initializes translations collection * - * @param object $translatable object to translate - * @param string $locale translation name - * @param array $properties object properties to translate - * @param string $class translation entity|document class - * @param Collection $coll translations collection + * @param object $translatable object to translate + * @param string $locale translation name + * @param string[] $properties object properties to translate + * @param string $class translation entity|document class + * + * @phpstan-param class-string $class + * @phpstan-param Collection $coll * * @throws \InvalidArgumentException Translation class doesn't implement TranslationInterface */ public function __construct($translatable, $locale, array $properties, $class, Collection $coll) { $this->translatable = $translatable; - $this->locale = $locale; - $this->properties = $properties; - $this->class = $class; - $this->coll = $coll; - - $translationClass = new \ReflectionClass($class); - if (!$translationClass->implementsInterface('Gedmo\Translator\TranslationInterface')) { - throw new \InvalidArgumentException(sprintf( - 'Translation class should implement Gedmo\Translator\TranslationInterface, "%s" given', - $class - )); + $this->locale = $locale; + $this->properties = $properties; + $this->class = $class; + $this->coll = $coll; + + if (!is_subclass_of($class, TranslationInterface::class)) { + throw new \InvalidArgumentException(sprintf('Translation class should implement %s, "%s" given', TranslationInterface::class, $class)); } } + /** + * @param string $method + * @param mixed[] $arguments + * + * @return mixed + */ public function __call($method, $arguments) { - $matches = array(); + $matches = []; if (preg_match('/^(set|get)(.*)$/', $method, $matches)) { $property = lcfirst($matches[2]); - if (in_array($property, $this->properties)) { + if (in_array($property, $this->properties, true)) { switch ($matches[1]) { case 'get': return $this->getTranslatedValue($property); @@ -69,7 +93,7 @@ public function __call($method, $arguments) } } - $return = call_user_func_array(array($this->translatable, $method), $arguments); + $return = call_user_func_array([$this->translatable, $method], $arguments); if ($this->translatable === $return) { return $this; @@ -78,9 +102,14 @@ public function __call($method, $arguments) return $return; } + /** + * @param string $property + * + * @return mixed + */ public function __get($property) { - if (in_array($property, $this->properties)) { + if (in_array($property, $this->properties, true)) { if (method_exists($this, $getter = 'get'.ucfirst($property))) { return $this->$getter; } @@ -91,22 +120,35 @@ public function __get($property) return $this->translatable->$property; } + /** + * @param string $property + * @param mixed $value + */ public function __set($property, $value) { - if (in_array($property, $this->properties)) { + if (in_array($property, $this->properties, true)) { if (method_exists($this, $setter = 'set'.ucfirst($property))) { - return $this->$setter($value); + $this->$setter($value); + + return; } - return $this->setTranslatedValue($property, $value); + $this->setTranslatedValue($property, $value); + + return; } $this->translatable->$property = $value; } + /** + * @param string $property + * + * @return bool + */ public function __isset($property) { - return in_array($property, $this->properties); + return in_array($property, $this->properties, true); } /** @@ -138,6 +180,8 @@ public function getTranslatedValue($property) * * @param string $property property name * @param string $value value + * + * @return void */ public function setTranslatedValue($property, $value) { @@ -148,13 +192,8 @@ public function setTranslatedValue($property, $value) /** * Finds existing or creates new translation for specified property - * - * @param string $property object property name - * @param string $locale locale name - * - * @return Translation */ - private function findOrCreateTranslationForProperty($property, $locale) + private function findOrCreateTranslationForProperty(string $property, string $locale): TranslationInterface { foreach ($this->coll as $translation) { if ($locale === $translation->getLocale() && $property === $translation->getProperty()) { @@ -163,7 +202,7 @@ private function findOrCreateTranslationForProperty($property, $locale) } /** @var TranslationInterface $translation */ - $translation = new $this->class; + $translation = new $this->class(); $translation->setTranslatable($this->translatable); $translation->setProperty($property); $translation->setLocale($locale); diff --git a/lib/Gedmo/Tree/Document/MongoDB/Repository/AbstractTreeRepository.php b/src/Tree/Document/MongoDB/Repository/AbstractTreeRepository.php similarity index 51% rename from lib/Gedmo/Tree/Document/MongoDB/Repository/AbstractTreeRepository.php rename to src/Tree/Document/MongoDB/Repository/AbstractTreeRepository.php index 2ee454c350..dfba75ef9a 100644 --- a/lib/Gedmo/Tree/Document/MongoDB/Repository/AbstractTreeRepository.php +++ b/src/Tree/Document/MongoDB/Repository/AbstractTreeRepository.php @@ -1,55 +1,71 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Document\MongoDB\Repository; -use Doctrine\ODM\MongoDB\DocumentRepository; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\Query\Builder; +use Doctrine\ODM\MongoDB\Query\Query; +use Doctrine\ODM\MongoDB\Repository\DocumentRepository; use Doctrine\ODM\MongoDB\UnitOfWork; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Tree\RepositoryInterface; use Gedmo\Tree\RepositoryUtils; use Gedmo\Tree\RepositoryUtilsInterface; -use Gedmo\Tree\RepositoryInterface; - +use Gedmo\Tree\TreeListener; + +/** + * @template T of object + * + * @phpstan-extends DocumentRepository + * + * @phpstan-implements RepositoryInterface + */ abstract class AbstractTreeRepository extends DocumentRepository implements RepositoryInterface { /** * Tree listener on event manager * - * @var AbstractTreeListener + * @var TreeListener */ - protected $listener = null; + protected $listener; /** * Repository utils + * + * @var RepositoryUtilsInterface */ - protected $repoUtils = null; + protected $repoUtils; - /** - * {@inheritdoc} - */ + /** @param ClassMetadata $class */ public function __construct(DocumentManager $em, UnitOfWork $uow, ClassMetadata $class) { parent::__construct($em, $uow, $class); $treeListener = null; - foreach ($em->getEventManager()->getListeners() as $listeners) { + foreach ($em->getEventManager()->getAllListeners() as $listeners) { foreach ($listeners as $listener) { - if ($listener instanceof \Gedmo\Tree\TreeListener) { + if ($listener instanceof TreeListener) { $treeListener = $listener; - break; + + break 2; } } - if ($treeListener) { - break; - } } - if (is_null($treeListener)) { - throw new \Gedmo\Exception\InvalidMappingException('This repository can be attached only to ODM MongoDB tree listener'); + if (null === $treeListener) { + throw new InvalidMappingException('This repository can be attached only to ODM MongoDB tree listener'); } $this->listener = $treeListener; if (!$this->validate()) { - throw new \Gedmo\Exception\InvalidMappingException('This repository cannot be used for tree type: '.$treeListener->getStrategy($em, $class->name)->getName()); + throw new InvalidMappingException('This repository cannot be used for tree type: '.$treeListener->getStrategy($em, $class->getName())->getName()); } $this->repoUtils = new RepositoryUtils($this->dm, $this->getClassMetadata(), $this->listener, $this); @@ -58,8 +74,6 @@ public function __construct(DocumentManager $em, UnitOfWork $uow, ClassMetadata /** * Sets the RepositoryUtilsInterface instance * - * @param \Gedmo\Tree\RepositoryUtilsInterface $repoUtils - * * @return $this */ public function setRepoUtils(RepositoryUtilsInterface $repoUtils) @@ -72,31 +86,25 @@ public function setRepoUtils(RepositoryUtilsInterface $repoUtils) /** * Returns the RepositoryUtilsInterface instance * - * @return \Gedmo\Tree\RepositoryUtilsInterface|null + * @return RepositoryUtilsInterface|null */ public function getRepoUtils() { return $this->repoUtils; } - /** - * {@inheritDoc} - */ - public function childrenHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false) + public function childrenHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->repoUtils->childrenHierarchy($node, $direct, $options, $includeNode); } - /** - * {@inheritDoc} - */ - public function buildTree(array $nodes, array $options = array()) + public function buildTree(array $nodes, array $options = []) { return $this->repoUtils->buildTree($nodes, $options); } /** - * @see \Gedmo\Tree\RepositoryUtilsInterface::setChildrenIndex + * @see RepositoryUtilsInterface::setChildrenIndex */ public function setChildrenIndex($childrenIndex) { @@ -104,96 +112,93 @@ public function setChildrenIndex($childrenIndex) } /** - * @see \Gedmo\Tree\RepositoryUtilsInterface::getChildrenIndex + * @see RepositoryUtilsInterface::getChildrenIndex */ public function getChildrenIndex() { return $this->repoUtils->getChildrenIndex(); } - /** - * {@inheritDoc} - */ public function buildTreeArray(array $nodes) { return $this->repoUtils->buildTreeArray($nodes); } - /** - * Checks if current repository is right - * for currently used tree strategy - * - * @return bool - */ - abstract protected function validate(); - /** * Get all root nodes query builder * - * @param string - Sort by field - * @param string - Sort direction ("asc" or "desc") + * @param string|null $sortByField Sort by field + * @param string $direction Sort direction ("asc" or "desc") * - * @return \Doctrine\MongoDB\Query\Builder - QueryBuilder object + * @return Builder */ abstract public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc'); /** * Get all root nodes query * - * @param string - Sort by field - * @param string - Sort direction ("asc" or "desc") + * @param string|null $sortByField Sort by field + * @param string $direction Sort direction ("asc" or "desc") * - * @return \Doctrine\MongoDB\Query\Query - Query object + * @return Query */ abstract public function getRootNodesQuery($sortByField = null, $direction = 'asc'); /** * Returns a QueryBuilder configured to return an array of nodes suitable for buildTree method * - * @param object $node - Root node - * @param bool $direct - Obtain direct children? - * @param array $options - Options - * @param boolean $includeNode - Include node in results? + * @param object $node Root node + * @param bool $direct Obtain direct children? + * @param array $options Options + * @param bool $includeNode Include node in results? * - * @return \Doctrine\MongoDB\Query\Builder - QueryBuilder object + * @return Builder */ - abstract public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = array(), $includeNode = false); + abstract public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false); /** * Returns a Query configured to return an array of nodes suitable for buildTree method * - * @param object $node - Root node - * @param bool $direct - Obtain direct children? - * @param array $options - Options - * @param boolean $includeNode - Include node in results? + * @param object $node Root node + * @param bool $direct Obtain direct children? + * @param array $options Options + * @param bool $includeNode Include node in results? * - * @return \Doctrine\MongoDB\Query\Query - Query object + * @return Query */ - abstract public function getNodesHierarchyQuery($node = null, $direct = false, array $options = array(), $includeNode = false); + abstract public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false); /** * Get list of children followed by given $node. This returns a QueryBuilder object * - * @param object $node - if null, all tree nodes will be taken - * @param boolean $direct - true to take only direct children - * @param string $sortByField - field name to sort by - * @param string $direction - sort direction : "ASC" or "DESC" - * @param bool $includeNode - Include the root node in results? + * @param object $node if null, all tree nodes will be taken + * @param bool $direct true to take only direct children + * @param string $sortByField field name to sort by + * @param string $direction sort direction : "ASC" or "DESC" + * @param bool $includeNode Include the root node in results? * - * @return \Doctrine\MongoDB\Query\Builder - QueryBuilder object + * @return Builder */ abstract public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); /** * Get list of children followed by given $node. This returns a Query * - * @param object $node - if null, all tree nodes will be taken - * @param boolean $direct - true to take only direct children - * @param string $sortByField - field name to sort by - * @param string $direction - sort direction : "ASC" or "DESC" - * @param bool $includeNode - Include the root node in results? + * @param object $node if null, all tree nodes will be taken + * @param bool $direct true to take only direct children + * @param string $sortByField field name to sort by + * @param string $direction sort direction : "ASC" or "DESC" + * @param bool $includeNode Include the root node in results? * - * @return \Doctrine\MongoDB\Query\Query - Query object + * @return Query */ abstract public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); + + /** + * Checks if current repository is right + * for currently used tree strategy + * + * @return bool + */ + abstract protected function validate(); } diff --git a/lib/Gedmo/Tree/Document/MongoDB/Repository/MaterializedPathRepository.php b/src/Tree/Document/MongoDB/Repository/MaterializedPathRepository.php similarity index 63% rename from lib/Gedmo/Tree/Document/MongoDB/Repository/MaterializedPathRepository.php rename to src/Tree/Document/MongoDB/Repository/MaterializedPathRepository.php index c4d8059faa..d135c914db 100644 --- a/lib/Gedmo/Tree/Document/MongoDB/Repository/MaterializedPathRepository.php +++ b/src/Tree/Document/MongoDB/Repository/MaterializedPathRepository.php @@ -1,10 +1,21 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Document\MongoDB\Repository; +use Doctrine\ODM\MongoDB\Iterator\Iterator; +use Doctrine\ODM\MongoDB\Query\Builder; +use Doctrine\ODM\MongoDB\Query\Query; use Gedmo\Exception\InvalidArgumentException; -use Gedmo\Tree\Strategy; use Gedmo\Tool\Wrapper\MongoDocumentWrapper; +use Gedmo\Tree\Strategy; +use MongoDB\BSON\Regex; /** * The MaterializedPathRepository has some useful functions @@ -13,16 +24,19 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @template T of object + * + * @template-extends AbstractTreeRepository */ class MaterializedPathRepository extends AbstractTreeRepository { /** * Get tree query builder * - * @param object $rootNode + * @param object|null $rootNode * - * @return \Doctrine\ODM\MongoDB\Query\Builder + * @return Builder */ public function getTreeQueryBuilder($rootNode = null) { @@ -32,9 +46,9 @@ public function getTreeQueryBuilder($rootNode = null) /** * Get tree query * - * @param object $rootNode + * @param object|null $rootNode * - * @return \Doctrine\ODM\MongoDB\Query\Query + * @return Query */ public function getTreeQuery($rootNode = null) { @@ -44,55 +58,43 @@ public function getTreeQuery($rootNode = null) /** * Get tree * - * @param object $rootNode + * @param object|null $rootNode * - * @return \Doctrine\ODM\MongoDB\Cursor + * @phpstan-return Iterator */ - public function getTree($rootNode = null) + public function getTree($rootNode = null): Iterator { - return $this->getTreeQuery($rootNode)->execute(); + return $this->getTreeQuery($rootNode)->getIterator(); } - /** - * {@inheritDoc} - */ public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') { return $this->getChildrenQueryBuilder(null, true, $sortByField, $direction); } - /** - * {@inheritDoc} - */ public function getRootNodesQuery($sortByField = null, $direction = 'asc') { return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); } - /** - * {@inheritDoc} - */ public function getRootNodes($sortByField = null, $direction = 'asc') { - return $this->getRootNodesQuery($sortByField, $direction)->execute(); + return $this->getRootNodesQuery($sortByField, $direction)->getIterator(); } - /** - * {@inheritDoc} - */ public function childCount($node = null, $direct = false) { $meta = $this->getClassMetadata(); if (is_object($node)) { - if (!($node instanceof $meta->name)) { - throw new InvalidArgumentException("Node is not related to this repository"); + if (!is_a($node, $meta->getName())) { + throw new InvalidArgumentException('Node is not related to this repository'); } $wrapped = new MongoDocumentWrapper($node, $this->dm); if (!$wrapped->hasValidIdentifier()) { - throw new InvalidArgumentException("Node is not managed by UnitOfWork"); + throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } } @@ -103,71 +105,68 @@ public function childCount($node = null, $direct = false) return (int) $qb->getQuery()->execute(); } - /** - * {@inheritDoc} - */ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->dm, $meta->name); + $config = $this->listener->getConfiguration($this->dm, $meta->getName()); $separator = preg_quote($config['path_separator']); $qb = $this->dm->createQueryBuilder() - ->find($meta->name); + ->find($meta->getName()); $regex = false; - if (is_object($node) && $node instanceof $meta->name) { + if (is_a($node, $meta->getName())) { $node = new MongoDocumentWrapper($node, $this->dm); $nodePath = preg_quote($node->getPropertyValue($config['path'])); if ($direct) { - $regex = sprintf('/^%s([^%s]+%s)'.($includeNode ? '?' : '').'$/', - $nodePath, - $separator, - $separator); + $regex = sprintf( + '^%s([^%s]+%s)'.($includeNode ? '?' : '').'$', + $nodePath, + $separator, + $separator + ); } else { - $regex = sprintf('/^%s(.+)'.($includeNode ? '?' : '').'/', - $nodePath); + $regex = sprintf( + '^%s(.+)'.($includeNode ? '?' : ''), + $nodePath + ); } } elseif ($direct) { - $regex = sprintf('/^([^%s]+)'.($includeNode ? '?' : '').'%s$/', + $regex = sprintf( + '^([^%s]+)'.($includeNode ? '?' : '').'%s$', $separator, - $separator); + $separator + ); } if ($regex) { - $qb->field($config['path'])->equals(new \MongoRegex($regex)); + $qb->field($config['path'])->equals(new Regex($regex)); } - $qb->sort(is_null($sortByField) ? $config['path'] : $sortByField, $direction === 'asc' ? 'asc' : 'desc'); + $qb->sort($sortByField ?? $config['path'], 'asc' === strtolower($direction) ? 'asc' : 'desc'); return $qb; } /** - * G{@inheritDoc} + * G{@inheritdoc} */ public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { return $this->getChildrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); } - /** - * {@inheritDoc} - */ public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { - return $this->getChildrenQuery($node, $direct, $sortByField, $direction, $includeNode)->execute(); + return $this->getChildrenQuery($node, $direct, $sortByField, $direction, $includeNode)->getIterator(); } - /** - * {@inheritDoc} - */ - public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = array(), $includeNode = false) + public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false) { - $sortBy = array( - 'field' => null, - 'dir' => 'asc', - ); + $sortBy = [ + 'field' => null, + 'dir' => 'asc', + ]; if (isset($options['childSort'])) { $sortBy = array_merge($sortBy, $options['childSort']); @@ -176,18 +175,12 @@ public function getNodesHierarchyQueryBuilder($node = null, $direct = false, arr return $this->getChildrenQueryBuilder($node, $direct, $sortBy['field'], $sortBy['dir'], $includeNode); } - /** - * {@inheritDoc} - */ - public function getNodesHierarchyQuery($node = null, $direct = false, array $options = array(), $includeNode = false) + public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); } - /** - * {@inheritDoc} - */ - public function getNodesHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false) + public function getNodesHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { $query = $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode); $query->setHydrate(false); @@ -195,11 +188,8 @@ public function getNodesHierarchy($node = null, $direct = false, array $options return $query->toArray(); } - /** - * {@inheritdoc} - */ protected function validate() { - return $this->listener->getStrategy($this->dm, $this->getClassMetadata()->name)->getName() === Strategy::MATERIALIZED_PATH; + return Strategy::MATERIALIZED_PATH === $this->listener->getStrategy($this->dm, $this->getClassMetadata()->name)->getName(); } } diff --git a/lib/Gedmo/Tree/Entity/MappedSuperclass/AbstractClosure.php b/src/Tree/Entity/MappedSuperclass/AbstractClosure.php similarity index 69% rename from lib/Gedmo/Tree/Entity/MappedSuperclass/AbstractClosure.php rename to src/Tree/Entity/MappedSuperclass/AbstractClosure.php index ef20c96f18..4bb1332e77 100644 --- a/lib/Gedmo/Tree/Entity/MappedSuperclass/AbstractClosure.php +++ b/src/Tree/Entity/MappedSuperclass/AbstractClosure.php @@ -1,42 +1,61 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Entity\MappedSuperclass; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\MappedSuperclass */ +#[ORM\MappedSuperclass] abstract class AbstractClosure { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] protected $id; /** * Mapped by listener * Visibility must be protected + * + * @var object|null */ protected $ancestor; /** * Mapped by listener * Visibility must be protected + * + * @var object|null */ protected $descendant; /** + * @var int|null + * * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] protected $depth; /** - * Get id - * - * @return integer + * @return int|null */ public function getId() { @@ -60,7 +79,7 @@ public function setAncestor($ancestor) /** * Get ancestor * - * @return object + * @return object|null */ public function getAncestor() { @@ -84,7 +103,7 @@ public function setDescendant($descendant) /** * Get descendant * - * @return object + * @return object|null */ public function getDescendant() { @@ -94,7 +113,7 @@ public function getDescendant() /** * Set depth * - * @param integer $depth + * @param int $depth * * @return static */ @@ -108,7 +127,7 @@ public function setDepth($depth) /** * Get depth * - * @return integer + * @return int|null */ public function getDepth() { diff --git a/src/Tree/Entity/Repository/AbstractTreeRepository.php b/src/Tree/Entity/Repository/AbstractTreeRepository.php new file mode 100644 index 0000000000..9b742c2efb --- /dev/null +++ b/src/Tree/Entity/Repository/AbstractTreeRepository.php @@ -0,0 +1,262 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree\Entity\Repository; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query; +use Doctrine\ORM\QueryBuilder; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Tool\Wrapper\EntityWrapper; +use Gedmo\Tree\RepositoryInterface; +use Gedmo\Tree\RepositoryUtils; +use Gedmo\Tree\RepositoryUtilsInterface; +use Gedmo\Tree\TreeListener; + +/** + * @template T of object + * + * @template-extends EntityRepository + * + * @template-implements RepositoryInterface + */ +abstract class AbstractTreeRepository extends EntityRepository implements RepositoryInterface +{ + /** + * Tree listener on event manager + * + * @var TreeListener + */ + protected $listener; + + /** + * Repository utils + * + * @var RepositoryUtilsInterface + */ + protected $repoUtils; + + /** @param ClassMetadata $class */ + public function __construct(EntityManagerInterface $em, ClassMetadata $class) + { + parent::__construct($em, $class); + $treeListener = null; + foreach ($em->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { + if ($listener instanceof TreeListener) { + $treeListener = $listener; + + break 2; + } + } + } + + if (null === $treeListener) { + throw new InvalidMappingException('Tree listener was not found on your entity manager, it must be hooked into the event manager'); + } + + $this->listener = $treeListener; + if (!$this->validate()) { + throw new InvalidMappingException('This repository cannot be used for tree type: '.$treeListener->getStrategy($em, $class->getName())->getName()); + } + + $this->repoUtils = new RepositoryUtils($this->getEntityManager(), $this->getClassMetadata(), $this->listener, $this); + } + + /** + * Sets the RepositoryUtilsInterface instance + * + * @return static + */ + public function setRepoUtils(RepositoryUtilsInterface $repoUtils) + { + $this->repoUtils = $repoUtils; + + return $this; + } + + /** + * Returns the RepositoryUtilsInterface instance + * + * @return RepositoryUtilsInterface|null + */ + public function getRepoUtils() + { + return $this->repoUtils; + } + + public function childCount($node = null, $direct = false) + { + $meta = $this->getClassMetadata(); + + if (is_object($node)) { + if (!is_a($node, $meta->getName())) { + throw new InvalidArgumentException('Node is not related to this repository'); + } + + $wrapped = new EntityWrapper($node, $this->getEntityManager()); + + if (!$wrapped->hasValidIdentifier()) { + throw new InvalidArgumentException('Node is not managed by UnitOfWork'); + } + } + + $qb = $this->getChildrenQueryBuilder($node, $direct); + + // We need to remove the ORDER BY DQL part since some vendors could throw an error + // in count queries + $dqlParts = $qb->getDQLParts(); + + // We need to check first if there's an ORDER BY DQL part, because resetDQLPart doesn't + // check if its internal array has an "orderby" index + if (isset($dqlParts['orderBy'])) { + $qb->resetDQLPart('orderBy'); + } + + $aliases = $qb->getRootAliases(); + $alias = $aliases[0]; + + $qb->select('COUNT('.$alias.')'); + + return (int) $qb->getQuery()->getSingleScalarResult(); + } + + /** + * @see RepositoryUtilsInterface::childrenHierarchy + */ + public function childrenHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) + { + return $this->repoUtils->childrenHierarchy($node, $direct, $options, $includeNode); + } + + /** + * @see RepositoryUtilsInterface::buildTree + */ + public function buildTree(array $nodes, array $options = []) + { + return $this->repoUtils->buildTree($nodes, $options); + } + + /** + * @see RepositoryUtilsInterface::buildTreeArray + */ + public function buildTreeArray(array $nodes) + { + return $this->repoUtils->buildTreeArray($nodes); + } + + /** + * @see RepositoryUtilsInterface::setChildrenIndex + */ + public function setChildrenIndex($childrenIndex) + { + $this->repoUtils->setChildrenIndex($childrenIndex); + } + + /** + * @see RepositoryUtilsInterface::getChildrenIndex + */ + public function getChildrenIndex() + { + return $this->repoUtils->getChildrenIndex(); + } + + /** + * Get all root nodes query builder + * + * @param string|string[]|null $sortByField Sort by field + * @param string|string[] $direction Sort direction ("asc" or "desc") + * + * @return QueryBuilder QueryBuilder object + */ + abstract public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc'); + + /** + * Get all root nodes query + * + * @param string|string[]|null $sortByField Sort by field + * @param string|string[] $direction Sort direction ("asc" or "desc") + * + * @return Query Query object + */ + abstract public function getRootNodesQuery($sortByField = null, $direction = 'asc'); + + /** + * Returns a QueryBuilder configured to return an array of nodes suitable for buildTree method + * + * @param object $node Root node + * @param bool $direct Obtain direct children? + * @param array $options Options + * @param bool $includeNode Include node in results? + * + * @return QueryBuilder QueryBuilder object + */ + abstract public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false); + + /** + * Returns a Query configured to return an array of nodes suitable for buildTree method + * + * @param object $node Root node + * @param bool $direct Obtain direct children? + * @param array $options Options + * @param bool $includeNode Include node in results? + * + * @return Query Query object + */ + abstract public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false); + + /** + * Get list of children followed by given $node. This returns a QueryBuilder object + * + * @param object|null $node If null, all tree nodes will be taken + * @param bool $direct True to take only direct children + * @param string|string[]|null $sortByField Field name or array of fields names to sort by + * @param string|string[] $direction Sort order ('asc'|'desc'|'ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements + * @param bool $includeNode Include the root node in results? + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC'|array $direction + * + * @return QueryBuilder QueryBuilder object + */ + abstract public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); + + /** + * Get list of children followed by given $node. This returns a Query + * + * @param object|null $node If null, all tree nodes will be taken + * @param bool $direct True to take only direct children + * @param string|string[]|null $sortByField Field name or array of fields names to sort by + * @param string|string[] $direction Sort order ('asc'|'desc'|'ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements + * @param bool $includeNode Include the root node in results? + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC'|array $direction + * + * @return Query Query object + */ + abstract public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); + + /** + * @return QueryBuilder + */ + protected function getQueryBuilder() + { + return $this->getEntityManager()->createQueryBuilder(); + } + + /** + * Checks if current repository is right + * for currently used tree strategy + * + * @return bool + */ + abstract protected function validate(); +} diff --git a/src/Tree/Entity/Repository/ClosureTreeRepository.php b/src/Tree/Entity/Repository/ClosureTreeRepository.php new file mode 100644 index 0000000000..9da2c49fe4 --- /dev/null +++ b/src/Tree/Entity/Repository/ClosureTreeRepository.php @@ -0,0 +1,645 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree\Entity\Repository; + +use Doctrine\ORM\Query; +use Doctrine\ORM\QueryBuilder; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Tool\Wrapper\EntityWrapper; +use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; +use Gedmo\Tree\Strategy; + +/** + * The ClosureTreeRepository has some useful functions + * to interact with Closure tree. Repository uses + * the strategy used by listener + * + * @author Gustavo Adrian + * @author Gediminas Morkevicius + * + * @template T of object + * + * @template-extends AbstractTreeRepository + */ +class ClosureTreeRepository extends AbstractTreeRepository +{ + /** Alias for the level value used in the subquery of the getNodesHierarchy method */ + public const SUBQUERY_LEVEL = 'level'; + + public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') + { + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $qb = $this->getQueryBuilder(); + $qb->select('node') + ->from($config['useObjectClass'], 'node') + ->where('node.'.$config['parent'].' IS NULL'); + + if (null !== $sortByField) { + $sortByField = (array) $sortByField; + $direction = (array) $direction; + foreach ($sortByField as $key => $field) { + $fieldDirection = $direction[$key] ?? 'asc'; + if ($meta->hasField($field) || $meta->isSingleValuedAssociation($field)) { + $qb->addOrderBy('node.'.$field, 'asc' === strtolower($fieldDirection) ? 'asc' : 'desc'); + } + } + } + + return $qb; + } + + public function getRootNodesQuery($sortByField = null, $direction = 'asc') + { + return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); + } + + public function getRootNodes($sortByField = null, $direction = 'asc') + { + return $this->getRootNodesQuery($sortByField, $direction)->getResult(); + } + + /** + * Get the Tree path query by given $node + * + * @param object $node + * + * @throws InvalidArgumentException if input is not valid + * + * @return Query + */ + public function getPathQuery($node) + { + $meta = $this->getClassMetadata(); + if (!is_a($node, $meta->getName())) { + throw new InvalidArgumentException('Node is not related to this repository'); + } + if (!$this->getEntityManager()->getUnitOfWork()->isInIdentityMap($node)) { + throw new InvalidArgumentException('Node is not managed by UnitOfWork'); + } + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $closureMeta = $this->getEntityManager()->getClassMetadata($config['closure']); + + $dql = "SELECT c, node FROM {$closureMeta->getName()} c"; + $dql .= ' INNER JOIN c.ancestor node'; + $dql .= ' WHERE c.descendant = :node'; + $dql .= ' ORDER BY c.depth DESC'; + $q = $this->getEntityManager()->createQuery($dql); + $q->setParameter('node', $node); + + return $q; + } + + /** + * Get the Tree path of Nodes by given $node + * + * @param object $node + * + * @return array list of Nodes in path + */ + public function getPath($node) + { + return array_map(static fn (AbstractClosure $closure) => $closure->getAncestor(), $this->getPathQuery($node)->getResult()); + } + + /** + * @param object|null $node If null, all tree nodes will be taken + * @param bool $direct True to take only direct children + * @param string|string[]|null $sortByField Field name or array of fields names to sort by + * @param string|string[] $direction Sort order ('asc'|'desc'|'ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements + * @param bool $includeNode Include the root node in results? + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC'|array $direction + * + * @return QueryBuilder QueryBuilder object + */ + public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + + $qb = $this->getQueryBuilder(); + if (null !== $node) { + if (is_a($node, $meta->getName())) { + if (!$this->getEntityManager()->getUnitOfWork()->isInIdentityMap($node)) { + throw new InvalidArgumentException('Node is not managed by UnitOfWork'); + } + + $where = 'c.ancestor = :node AND '; + + $qb->select('c, node') + ->from($config['closure'], 'c') + ->innerJoin('c.descendant', 'node'); + + if ($direct) { + $where .= 'c.depth = 1'; + } else { + $where .= 'c.descendant <> :node'; + } + + $qb->where($where); + + if ($includeNode) { + $qb->orWhere('c.ancestor = :node AND c.descendant = :node'); + } + } else { + throw new \InvalidArgumentException('Node is not related to this repository'); + } + } else { + $qb->select('node') + ->from($config['useObjectClass'], 'node'); + if ($direct) { + $qb->where('node.'.$config['parent'].' IS NULL'); + } + } + + if ($sortByField) { + if (is_array($sortByField)) { + foreach ($sortByField as $key => $field) { + $fieldDirection = is_array($direction) ? ($direction[$key] ?? 'asc') : $direction; + if (($meta->hasField($field) || $meta->isSingleValuedAssociation($field)) && in_array(strtolower($fieldDirection), ['asc', 'desc'], true)) { + $qb->addOrderBy('node.'.$field, $fieldDirection); + } else { + throw new InvalidArgumentException(sprintf('Invalid sort options specified: field - %s, direction - %s', $field, $fieldDirection)); + } + } + } else { + if (($meta->hasField($sortByField) || $meta->isSingleValuedAssociation($sortByField)) && in_array(strtolower($direction), ['asc', 'desc'], true)) { + $qb->orderBy('node.'.$sortByField, $direction); + } else { + throw new InvalidArgumentException(sprintf('Invalid sort options specified: field - %s, direction - %s', $sortByField, $direction)); + } + } + } + + if ($node) { + $qb->setParameter('node', $node); + } + + return $qb; + } + + /** + * @param object|null $node If null, all tree nodes will be taken + * @param bool $direct True to take only direct children + * @param string|string[]|null $sortByField Field name or array of fields names to sort by + * @param string|string[] $direction Sort order ('asc'|'desc'|'ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements + * @param bool $includeNode Include the root node in results? + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC'|array $direction + * + * @return Query Query object + */ + public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); + } + + /** + * @param object|null $node If null, all tree nodes will be taken + * @param bool $direct True to take only direct children + * @param string|string[]|null $sortByField Field name or array of fields names to sort by + * @param string|string[] $direction Sort order ('asc'|'desc'|'ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements + * @param bool $includeNode Include the root node in results? + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC'|array $direction + * + * @return array List of children or null on failure + */ + public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + $result = $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode)->getResult(); + if ($node) { + $result = array_map(static fn (AbstractClosure $closure) => $closure->getDescendant(), $result); + } + + return $result; + } + + public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode); + } + + public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode); + } + + /** + * @return array + */ + public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + return $this->children($node, $direct, $sortByField, $direction, $includeNode); + } + + /** + * Removes given $node from the tree and reparents its descendants + * + * @todo may be improved, to issue single query on reparenting + * + * @param object $node + * + * @throws InvalidArgumentException + * @throws \Gedmo\Exception\RuntimeException if something fails in transaction + * + * @return void + */ + public function removeFromTree($node) + { + $meta = $this->getClassMetadata(); + if (!is_a($node, $meta->getName())) { + throw new InvalidArgumentException('Node is not related to this repository'); + } + $wrapped = new EntityWrapper($node, $this->getEntityManager()); + if (!$wrapped->hasValidIdentifier()) { + throw new InvalidArgumentException('Node is not managed by UnitOfWork'); + } + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $pk = $meta->getSingleIdentifierFieldName(); + $nodeId = $wrapped->getIdentifier(); + $parent = $wrapped->getPropertyValue($config['parent']); + + $dql = "SELECT node FROM {$config['useObjectClass']} node"; + $dql .= " WHERE node.{$config['parent']} = :node"; + $q = $this->getEntityManager()->createQuery($dql); + $q->setParameter('node', $node); + $nodesToReparent = $q->toIterable(); + // process updates in transaction + $this->getEntityManager()->getConnection()->beginTransaction(); + + try { + foreach ($nodesToReparent as $nodeToReparent) { + $id = $meta->getFieldValue($nodeToReparent, $pk); + $meta->setFieldValue($nodeToReparent, $config['parent'], $parent); + + $dql = "UPDATE {$config['useObjectClass']} node"; + $dql .= " SET node.{$config['parent']} = :parent"; + $dql .= " WHERE node.{$pk} = :id"; + + $q = $this->getEntityManager()->createQuery($dql); + $q->setParameters([ + 'parent' => $parent, + 'id' => $id, + ]); + $q->getSingleScalarResult(); + + $this->listener + ->getStrategy($this->getEntityManager(), $meta->getName()) + ->updateNode($this->getEntityManager(), $nodeToReparent, $node); + + $oid = spl_object_id($nodeToReparent); + $this->getEntityManager()->getUnitOfWork()->setOriginalEntityProperty($oid, $config['parent'], $parent); + } + + $dql = "DELETE {$config['useObjectClass']} node"; + $dql .= " WHERE node.{$pk} = :nodeId"; + + $q = $this->getEntityManager()->createQuery($dql); + $q->setParameter('nodeId', $nodeId); + $q->getSingleScalarResult(); + $this->getEntityManager()->getConnection()->commit(); + } catch (\Exception $e) { + $this->getEntityManager()->close(); + $this->getEntityManager()->getConnection()->rollback(); + + throw new \Gedmo\Exception\RuntimeException('Transaction failed: '.$e->getMessage(), $e->getCode(), $e); + } + // remove from identity map + $this->getEntityManager()->getUnitOfWork()->removeFromIdentityMap($node); + $node = null; + } + + public function buildTreeArray(array $nodes) + { + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $nestedTree = []; + $idField = $meta->getSingleIdentifierFieldName(); + $hasLevelProp = !empty($config['level']); + $levelProp = $hasLevelProp ? $config['level'] : self::SUBQUERY_LEVEL; + $childrenIndex = $this->repoUtils->getChildrenIndex(); + + if ([] !== $nodes) { + $firstLevel = $hasLevelProp ? $nodes[0][0]['descendant'][$levelProp] : $nodes[0][$levelProp]; + $l = 1; // 1 is only an initial value. We could have a tree which has a root node with any level (subtrees) + $refs = []; + + foreach ($nodes as $n) { + $node = $n[0]['descendant']; + $node[$childrenIndex] = []; + $level = $hasLevelProp ? $node[$levelProp] : $n[$levelProp]; + + if ($l < $level) { + $l = $level; + } + + if ($l == $firstLevel) { + $tmp = &$nestedTree; + } else { + $tmp = &$refs[$n['parent_id']][$childrenIndex]; + } + + $key = count($tmp); + $tmp[$key] = $node; + $refs[$node[$idField]] = &$tmp[$key]; + } + + unset($refs); + } + + return $nestedTree; + } + + public function getNodesHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) + { + return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult(); + } + + public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false) + { + return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); + } + + public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false) + { + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $idField = $meta->getSingleIdentifierFieldName(); + $subQuery = ''; + $hasLevelProp = isset($config['level']) && $config['level']; + + if (!$hasLevelProp) { + $subQuery = ', (SELECT MAX(c2.depth) + 1 FROM '.$config['closure']; + $subQuery .= ' c2 WHERE c2.descendant = c.descendant GROUP BY c2.descendant) AS '.self::SUBQUERY_LEVEL; + } + + $q = $this->getEntityManager()->createQueryBuilder() + ->select('c, node, p.'.$idField.' AS parent_id'.$subQuery) + ->from($config['closure'], 'c') + ->innerJoin('c.descendant', 'node') + ->leftJoin('node.parent', 'p') + ->addOrderBy($hasLevelProp ? 'node.'.$config['level'] : self::SUBQUERY_LEVEL, 'asc'); + + if (null !== $node) { + $q->where('c.ancestor = :node'); + $q->setParameter('node', $node); + } else { + $q->groupBy('c.descendant'); + } + + if (!$includeNode) { + $q->andWhere('c.ancestor != c.descendant'); + } + + $defaultOptions = []; + $options = array_merge($defaultOptions, $options); + + if (isset($options['childSort']) && is_array($options['childSort']) + && isset($options['childSort']['field'], $options['childSort']['dir'])) { + $q->addOrderBy( + 'node.'.$options['childSort']['field'], + 'asc' === strtolower($options['childSort']['dir']) ? 'asc' : 'desc' + ); + } + + return $q; + } + + /** + * @return array|bool + */ + public function verify() + { + $nodeMeta = $this->getClassMetadata(); + $nodeIdField = $nodeMeta->getSingleIdentifierFieldName(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $nodeMeta->getName()); + $closureMeta = $this->getEntityManager()->getClassMetadata($config['closure']); + $errors = []; + + $q = $this->getEntityManager()->createQuery(" + SELECT COUNT(node) + FROM {$nodeMeta->getName()} AS node + LEFT JOIN {$closureMeta->getName()} AS c WITH c.ancestor = node AND c.depth = 0 + WHERE c.id IS NULL + "); + + if ($missingSelfRefsCount = (int) $q->getSingleScalarResult()) { + $errors[] = "Missing $missingSelfRefsCount self referencing closures"; + } + + $q = $this->getEntityManager()->createQuery(" + SELECT COUNT(node) + FROM {$nodeMeta->getName()} AS node + INNER JOIN {$closureMeta->getName()} AS c1 WITH c1.descendant = node.{$config['parent']} + LEFT JOIN {$closureMeta->getName()} AS c2 WITH c2.descendant = node.$nodeIdField AND c2.ancestor = c1.ancestor + WHERE c2.id IS NULL AND node.$nodeIdField <> c1.ancestor + "); + + if ($missingClosuresCount = (int) $q->getSingleScalarResult()) { + $errors[] = "Missing $missingClosuresCount closures"; + } + + $q = $this->getEntityManager()->createQuery(" + SELECT COUNT(c1.id) + FROM {$closureMeta->getName()} AS c1 + LEFT JOIN {$nodeMeta->getName()} AS node WITH c1.descendant = node.$nodeIdField + LEFT JOIN {$closureMeta->getName()} AS c2 WITH c2.descendant = node.{$config['parent']} AND c2.ancestor = c1.ancestor + WHERE c2.id IS NULL AND c1.descendant <> c1.ancestor + "); + + if ($invalidClosuresCount = (int) $q->getSingleScalarResult()) { + $errors[] = "Found $invalidClosuresCount invalid closures"; + } + + if (!empty($config['level'])) { + $levelField = $config['level']; + $maxResults = 1000; + $q = $this->getEntityManager()->createQuery(" + SELECT node.$nodeIdField AS id, node.$levelField AS node_level, MAX(c.depth) AS closure_level + FROM {$nodeMeta->getName()} AS node + INNER JOIN {$closureMeta->getName()} AS c WITH c.descendant = node.$nodeIdField + GROUP BY node.$nodeIdField, node.$levelField + HAVING node.$levelField IS NULL OR node.$levelField <> MAX(c.depth) + 1 + ")->setMaxResults($maxResults); + + if ($invalidLevelsCount = count($q->getScalarResult())) { + $errors[] = "Found $invalidLevelsCount invalid level values"; + } + } + + return [] !== $errors ? $errors : true; + } + + /** + * @return void + */ + public function recover() + { + if (true === $this->verify()) { + return; + } + + $this->cleanUpClosure(); + $this->rebuildClosure(); + } + + /** + * @return int + */ + public function rebuildClosure() + { + $nodeMeta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $nodeMeta->getName()); + $closureMeta = $this->getEntityManager()->getClassMetadata($config['closure']); + + $insertClosures = function ($entries) use ($closureMeta) { + $closureTable = $closureMeta->getTableName(); + $ancestorColumnName = $this->getJoinColumnFieldName($closureMeta->getAssociationMapping('ancestor')); + $descendantColumnName = $this->getJoinColumnFieldName($closureMeta->getAssociationMapping('descendant')); + $depthColumnName = $closureMeta->getColumnName('depth'); + + $conn = $this->getEntityManager()->getConnection(); + $conn->beginTransaction(); + foreach ($entries as $entry) { + $conn->insert($closureTable, array_combine( + [$ancestorColumnName, $descendantColumnName, $depthColumnName], + $entry + )); + } + $conn->commit(); + }; + + $buildClosures = function ($dql) use ($insertClosures) { + $newClosuresCount = 0; + $batchSize = 1000; + $q = $this->getEntityManager()->createQuery($dql)->setMaxResults($batchSize)->setCacheable(false); + do { + $entries = $q->getScalarResult(); + $insertClosures($entries); + $newClosuresCount += count($entries); + } while ([] !== $entries); + + return $newClosuresCount; + }; + + $nodeIdField = $nodeMeta->getSingleIdentifierFieldName(); + $newClosuresCount = $buildClosures(" + SELECT node.$nodeIdField AS ancestor, node.$nodeIdField AS descendant, 0 AS depth + FROM {$nodeMeta->getName()} AS node + LEFT JOIN {$closureMeta->getName()} AS c WITH c.ancestor = node AND c.depth = 0 + WHERE c.id IS NULL + "); + $newClosuresCount += $buildClosures(" + SELECT IDENTITY(c1.ancestor) AS ancestor, node.$nodeIdField AS descendant, c1.depth + 1 AS depth + FROM {$nodeMeta->getName()} AS node + INNER JOIN {$closureMeta->getName()} AS c1 WITH c1.descendant = node.{$config['parent']} + LEFT JOIN {$closureMeta->getName()} AS c2 WITH c2.descendant = node.$nodeIdField AND c2.ancestor = c1.ancestor + WHERE c2.id IS NULL AND node.$nodeIdField <> c1.ancestor + "); + + return $newClosuresCount; + } + + /** + * @return int + */ + public function cleanUpClosure() + { + $conn = $this->getEntityManager()->getConnection(); + $nodeMeta = $this->getClassMetadata(); + $nodeIdField = $nodeMeta->getSingleIdentifierFieldName(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $nodeMeta->getName()); + $closureMeta = $this->getEntityManager()->getClassMetadata($config['closure']); + $closureTableName = $closureMeta->getTableName(); + + $dql = " + SELECT c1.id AS id + FROM {$closureMeta->getName()} AS c1 + LEFT JOIN {$nodeMeta->getName()} AS node WITH c1.descendant = node.$nodeIdField + LEFT JOIN {$closureMeta->getName()} AS c2 WITH c2.descendant = node.{$config['parent']} AND c2.ancestor = c1.ancestor + WHERE c2.id IS NULL AND c1.descendant <> c1.ancestor + "; + + $deletedClosuresCount = 0; + $batchSize = 1000; + $q = $this->getEntityManager()->createQuery($dql)->setMaxResults($batchSize)->setCacheable(false); + + while (($ids = $q->getScalarResult()) && [] !== $ids) { + $ids = array_map(static fn (array $el) => $el['id'], $ids); + $query = "DELETE FROM {$closureTableName} WHERE id IN (".implode(', ', $ids).')'; + if (0 === $conn->executeStatement($query)) { + throw new \RuntimeException('Failed to remove incorrect closures'); + } + $deletedClosuresCount += count($ids); + } + + return $deletedClosuresCount; + } + + /** + * @return int + */ + public function updateLevelValues() + { + $nodeMeta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $nodeMeta->getName()); + $levelUpdatesCount = 0; + + if (!empty($config['level'])) { + $levelField = $config['level']; + $nodeIdField = $nodeMeta->getSingleIdentifierFieldName(); + $closureMeta = $this->getEntityManager()->getClassMetadata($config['closure']); + + $batchSize = 1000; + $q = $this->getEntityManager()->createQuery(" + SELECT node.$nodeIdField AS id, node.$levelField AS node_level, MAX(c.depth) AS closure_level + FROM {$nodeMeta->getName()} AS node + INNER JOIN {$closureMeta->getName()} AS c WITH c.descendant = node.$nodeIdField + GROUP BY node.$nodeIdField, node.$levelField + HAVING node.$levelField IS NULL OR node.$levelField <> MAX(c.depth) + 1 + ")->setMaxResults($batchSize)->setCacheable(false); + do { + $entries = $q->getScalarResult(); + $this->getEntityManager()->getConnection()->beginTransaction(); + foreach ($entries as $entry) { + unset($entry['node_level']); + $this->getEntityManager()->createQuery(" + UPDATE {$nodeMeta->getName()} AS node SET node.$levelField = (:closure_level + 1) WHERE node.$nodeIdField = :id + ")->execute($entry); + } + $this->getEntityManager()->getConnection()->commit(); + $levelUpdatesCount += count($entries); + } while ([] !== $entries); + } + + return $levelUpdatesCount; + } + + protected function validate() + { + return Strategy::CLOSURE === $this->listener->getStrategy($this->getEntityManager(), $this->getClassMetadata()->name)->getName(); + } + + /** + * @param array $association + * + * @return string|null + */ + protected function getJoinColumnFieldName($association) + { + if (count($association['joinColumnFieldNames']) > 1) { + throw new \RuntimeException('More association on field '.$association['fieldName']); + } + + return array_shift($association['joinColumnFieldNames']); + } +} diff --git a/lib/Gedmo/Tree/Entity/Repository/MaterializedPathRepository.php b/src/Tree/Entity/Repository/MaterializedPathRepository.php similarity index 78% rename from lib/Gedmo/Tree/Entity/Repository/MaterializedPathRepository.php rename to src/Tree/Entity/Repository/MaterializedPathRepository.php index 4135806e95..88d416d9e6 100644 --- a/lib/Gedmo/Tree/Entity/Repository/MaterializedPathRepository.php +++ b/src/Tree/Entity/Repository/MaterializedPathRepository.php @@ -1,9 +1,18 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Entity\Repository; -use Gedmo\Tree\Strategy; +use Doctrine\ORM\Query; +use Doctrine\ORM\QueryBuilder; use Gedmo\Tool\Wrapper\EntityWrapper; +use Gedmo\Tree\Strategy; /** * The MaterializedPathRepository has some useful functions @@ -12,7 +21,10 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @template T of object + * + * @template-extends AbstractTreeRepository */ class MaterializedPathRepository extends AbstractTreeRepository { @@ -21,11 +33,11 @@ class MaterializedPathRepository extends AbstractTreeRepository * * @param object $rootNode * - * @return \Doctrine\ORM\QueryBuilder + * @return QueryBuilder */ public function getTreeQueryBuilder($rootNode = null) { - return $this->getChildrenQueryBuilder($rootNode, false, null, 'asc', true); + return $this->getChildrenQueryBuilder($rootNode, false, null, 'ASC', true); } /** @@ -33,7 +45,7 @@ public function getTreeQueryBuilder($rootNode = null) * * @param object $rootNode * - * @return \Doctrine\ORM\Query + * @return Query */ public function getTreeQuery($rootNode = null) { @@ -45,35 +57,26 @@ public function getTreeQuery($rootNode = null) * * @param object $rootNode * - * @return array + * @return array */ public function getTree($rootNode = null) { - return $this->getTreeQuery($rootNode)->execute(); + return $this->getTreeQuery($rootNode)->getResult(); } - /** - * {@inheritDoc} - */ public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') { return $this->getChildrenQueryBuilder(null, true, $sortByField, $direction); } - /** - * {@inheritDoc} - */ public function getRootNodesQuery($sortByField = null, $direction = 'asc') { return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); } - /** - * {@inheritDoc} - */ public function getRootNodes($sortByField = null, $direction = 'asc') { - return $this->getRootNodesQuery($sortByField, $direction)->execute(); + return $this->getRootNodesQuery($sortByField, $direction)->getResult(); } /** @@ -81,18 +84,18 @@ public function getRootNodes($sortByField = null, $direction = 'asc') * * @param object $node * - * @return \Doctrine\ORM\QueryBuilder + * @return QueryBuilder */ public function getPathQueryBuilder($node) { $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); $alias = 'materialized_path_entity'; $qb = $this->getQueryBuilder() ->select($alias) ->from($config['useObjectClass'], $alias); - $node = new EntityWrapper($node, $this->_em); + $node = new EntityWrapper($node, $this->getEntityManager()); $nodePath = $node->getPropertyValue($config['path']); $paths = []; $nodePathLength = strlen($nodePath); @@ -100,11 +103,11 @@ public function getPathQueryBuilder($node) while ($separatorMatchOffset < $nodePathLength) { $separatorPos = strpos($nodePath, $config['path_separator'], $separatorMatchOffset); - if ($separatorPos === false || $separatorPos === $nodePathLength - 1) { + if (false === $separatorPos || $separatorPos === $nodePathLength - 1) { // last node, done $paths[] = $nodePath; $separatorMatchOffset = $nodePathLength; - } elseif ($separatorPos === 0) { + } elseif (0 === $separatorPos) { // path starts with separator, continue $separatorMatchOffset = 1; } else { @@ -127,7 +130,7 @@ public function getPathQueryBuilder($node) * * @param object $node * - * @return \Doctrine\ORM\Query + * @return Query */ public function getPathQuery($node) { @@ -139,20 +142,17 @@ public function getPathQuery($node) * * @param object $node * - * @return array - list of Nodes in path + * @return array list of Nodes in path */ public function getPath($node) { return $this->getPathQuery($node)->getResult(); } - /** - * {@inheritDoc} - */ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); $separator = addcslashes($config['path_separator'], '%'); $alias = 'materialized_path_entity'; $path = $config['path']; @@ -162,8 +162,8 @@ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByFi $expr = ''; $includeNodeExpr = ''; - if (is_object($node) && $node instanceof $meta->name) { - $node = new EntityWrapper($node, $this->_em); + if (is_a($node, $meta->getName())) { + $node = new EntityWrapper($node, $this->getEntityManager()); $nodePath = $node->getPropertyValue($path); $expr = $qb->expr()->andx()->add( $qb->expr()->like( @@ -209,38 +209,29 @@ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByFi $qb->orWhere('('.$includeNodeExpr.')'); } - $orderByField = is_null($sortByField) ? $alias.'.'.$config['path'] : $alias.'.'.$sortByField; - $orderByDir = $direction === 'asc' ? 'asc' : 'desc'; + $orderByField = null === $sortByField ? $alias.'.'.$config['path'] : $alias.'.'.$sortByField; + $orderByDir = 'asc' === strtolower($direction) ? 'asc' : 'desc'; $qb->orderBy($orderByField, $orderByDir); return $qb; } - /** - * {@inheritDoc} - */ public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { return $this->getChildrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); } - /** - * {@inheritDoc} - */ public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { - return $this->getChildrenQuery($node, $direct, $sortByField, $direction, $includeNode)->execute(); + return $this->getChildrenQuery($node, $direct, $sortByField, $direction, $includeNode)->getResult(); } - /** - * {@inheritdoc} - */ - public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = array(), $includeNode = false) + public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false) { - $sortBy = array( - 'field' => null, - 'dir' => 'asc', - ); + $sortBy = [ + 'field' => null, + 'dir' => 'asc', + ]; if (isset($options['childSort'])) { $sortBy = array_merge($sortBy, $options['childSort']); @@ -249,38 +240,28 @@ public function getNodesHierarchyQueryBuilder($node = null, $direct = false, arr return $this->getChildrenQueryBuilder($node, $direct, $sortBy['field'], $sortBy['dir'], $includeNode); } - /** - * {@inheritdoc} - */ - public function getNodesHierarchyQuery($node = null, $direct = false, array $options = array(), $includeNode = false) + public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); } - /** - * {@inheritdoc} - */ - public function getNodesHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false) + public function getNodesHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->_em, $meta->name); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); $path = $config['path']; $nodes = $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult(); usort( $nodes, - function ($a, $b) use ($path) { - return strcmp($a[$path], $b[$path]); - } + static fn (array $a, array $b): int => strcmp($a[$path], $b[$path]) ); + return $nodes; } - /** - * {@inheritdoc} - */ protected function validate() { - return $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName() === Strategy::MATERIALIZED_PATH; + return Strategy::MATERIALIZED_PATH === $this->listener->getStrategy($this->getEntityManager(), $this->getClassMetadata()->name)->getName(); } } diff --git a/src/Tree/Entity/Repository/NestedTreeRepository.php b/src/Tree/Entity/Repository/NestedTreeRepository.php new file mode 100644 index 0000000000..375f42d6a6 --- /dev/null +++ b/src/Tree/Entity/Repository/NestedTreeRepository.php @@ -0,0 +1,1388 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree\Entity\Repository; + +use Doctrine\Deprecations\Deprecation; +use Doctrine\ORM\Exception\ORMException; +use Doctrine\ORM\Query; +use Doctrine\ORM\QueryBuilder; +use Doctrine\Persistence\Proxy; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Exception\RuntimeException; +use Gedmo\Exception\UnexpectedValueException; +use Gedmo\Tool\ORM\Repository\EntityRepositoryCompat; +use Gedmo\Tool\Wrapper\EntityWrapper; +use Gedmo\Tree\Node; +use Gedmo\Tree\Strategy; +use Gedmo\Tree\Strategy\ORM\Nested; + +/** + * The NestedTreeRepository has some useful functions + * to interact with NestedSet tree. Repository uses + * the strategy used by listener + * + * @author Gediminas Morkevicius + * + * @template T of object + * + * @template-extends AbstractTreeRepository + * + * @method persistAsFirstChild($node) + * @method persistAsFirstChildOf($node, $parent) + * @method persistAsLastChild($node) + * @method persistAsLastChildOf($node, $parent) + * @method persistAsNextSibling($node) + * @method persistAsNextSiblingOf($node, $sibling) + * @method persistAsPrevSibling($node) + * @method persistAsPrevSiblingOf($node, $sibling) + */ +class NestedTreeRepository extends AbstractTreeRepository +{ + use EntityRepositoryCompat; + + public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') + { + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $qb = $this->getQueryBuilder(); + $qb + ->select('node') + ->from($config['useObjectClass'], 'node') + ->where($qb->expr()->isNull('node.'.$config['parent'])) + ; + + if (null !== $sortByField) { + $sortByField = (array) $sortByField; + $direction = (array) $direction; + foreach ($sortByField as $key => $field) { + $fieldDirection = $direction[$key] ?? 'asc'; + if ($meta->hasField($field) || $meta->isSingleValuedAssociation($field)) { + $qb->addOrderBy('node.'.$field, 'asc' === strtolower($fieldDirection) ? 'asc' : 'desc'); + } + } + } else { + $qb->orderBy('node.'.$config['left'], 'ASC'); + } + + return $qb; + } + + public function getRootNodesQuery($sortByField = null, $direction = 'asc') + { + return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); + } + + public function getRootNodes($sortByField = null, $direction = 'asc') + { + return $this->getRootNodesQuery($sortByField, $direction)->getResult(); + } + + /** + * Get the Tree path query builder by given $node + * + * @param object $node + * + * @phpstan-param array{includeNode?: bool} $options + * + * options: + * - includeNode: (bool) Whether to include the node itself. Defaults to true. + * + * @throws InvalidArgumentException if input is not valid + * + * @return QueryBuilder + */ + public function getPathQueryBuilder($node/* , array $options = [] */) // @phpstan-ignore-line + { + $options = func_get_args()[1] ?? []; + if (!\is_array($options)) { + throw new \TypeError('Argument 2 MUST be an array.'); + } + + $defaultOptions = [ + 'includeNode' => true, + ]; + $options += $defaultOptions; + + $meta = $this->getClassMetadata(); + if (!is_a($node, $meta->getName())) { + throw new InvalidArgumentException('Node is not related to this repository'); + } + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $wrapped = new EntityWrapper($node, $this->getEntityManager()); + if (!$wrapped->hasValidIdentifier()) { + throw new InvalidArgumentException('Node is not managed by UnitOfWork'); + } + $left = $wrapped->getPropertyValue($config['left']); + $right = $wrapped->getPropertyValue($config['right']); + $qb = $this->getQueryBuilder(); + $qb->select('node') + ->from($config['useObjectClass'], 'node') + ->orderBy('node.'.$config['left'], 'ASC') + ; + if ($options['includeNode']) { + $qb->where($qb->expr()->lte('node.'.$config['left'], $left)) + ->andWhere($qb->expr()->gte('node.'.$config['right'], $right)); + } else { + $qb->where($qb->expr()->lt('node.'.$config['left'], $left)) + ->andWhere($qb->expr()->gt('node.'.$config['right'], $right)); + } + if (isset($config['root'])) { + $rootId = $wrapped->getPropertyValue($config['root']); + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $rootId); + } + + return $qb; + } + + /** + * Get the Tree path query by given $node + * + * @param object $node + * + * @phpstan-param array{includeNode?: bool} $options + * + * options: + * - includeNode: (bool) Whether to include the node itself. Defaults to true. + * + * @return Query + */ + public function getPathQuery($node/* , array $options = [] */) // @phpstan-ignore-line + { + $options = func_get_args()[1] ?? []; + if (!\is_array($options)) { + throw new \TypeError('Argument 2 MUST be an array.'); + } + + return $this->getPathQueryBuilder($node, $options)->getQuery(); + } + + /** + * Get the Tree path of Nodes by given $node + * + * @param object $node + * + * @phpstan-param array{includeNode?: bool} $options + * + * options: + * - includeNode: (bool) Whether to include the node itself. Defaults to true. + * + * @return array list of Nodes in path + */ + public function getPath($node/* , array $options = [] */) // @phpstan-ignore-line + { + $options = func_get_args()[1] ?? []; + if (!\is_array($options)) { + throw new \TypeError('Argument 2 MUST be an array.'); + } + + return $this->getPathQuery($node, $options)->getResult(); + } + + /** + * Get the Tree path of Nodes by given $node as a string + * + * @phpstan-param array{ + * includeNode?: bool, + * separator?: string, + * stringMethod?: string + * } $options + * + * options: + * - includeNode: (bool) Whether to include the node itself. Defaults to true. + * - separator: (string) The string separating the nodes of the tree. Defaults to ' > '. + * - stringMethod: (string) Entity method returning its displayable name. Defaults to '__toString'. + * + * @throws InvalidArgumentException + */ + public function getPathAsString(object $node, array $options = []): string + { + $defaultOptions = [ + 'includeNode' => true, + 'separator' => ' > ', + 'stringMethod' => '__toString', + ]; + $options += $defaultOptions; + + if (!is_string($options['stringMethod'])) { + throw new InvalidArgumentException(sprintf('"stringMethod" option passed in argument 2 to %s must be a valid string.', __METHOD__)); + } + if (!method_exists($node, $options['stringMethod'])) { + throw new InvalidArgumentException(sprintf('%s must implement method "%s".', get_class($node), $options['stringMethod'])); + } + + $path = []; + foreach ($this->getPath($node, $options) as $pathNode) { + $path[] = $pathNode->{$options['stringMethod']}(); + } + + return implode($options['separator'], $path); + } + + /** + * @param object|null $node If null, all tree nodes will be taken + * @param bool $direct True to take only direct children + * @param string|string[]|null $sortByField Field name or array of fields names to sort by + * @param string|string[] $direction Sort order ('asc'|'desc'|'ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements + * @param bool $includeNode Include the root node in results? + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC'|array $direction + * + * @return QueryBuilder QueryBuilder object + */ + public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + + $qb = $this->getQueryBuilder(); + $qb->select('node') + ->from($config['useObjectClass'], 'node') + ; + if (null !== $node) { + if (is_a($node, $meta->getName())) { + $wrapped = new EntityWrapper($node, $this->getEntityManager()); + if (!$wrapped->hasValidIdentifier()) { + throw new InvalidArgumentException('Node is not managed by UnitOfWork'); + } + if ($direct) { + $qb->where($qb->expr()->eq('node.'.$config['parent'], ':pid')); + $qb->setParameter('pid', $wrapped->getIdentifier()); + } else { + $left = $wrapped->getPropertyValue($config['left']); + $right = $wrapped->getPropertyValue($config['right']); + if ($left && $right) { + $qb->where($qb->expr()->lt('node.'.$config['right'], $right)); + $qb->andWhere($qb->expr()->gt('node.'.$config['left'], $left)); + } + } + if (isset($config['root'])) { + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $wrapped->getPropertyValue($config['root'])); + } + if ($includeNode) { + $idField = $meta->getSingleIdentifierFieldName(); + $qb->where('('.$qb->getDqlPart('where').') OR node.'.$idField.' = :rootNode'); + $qb->setParameter('rootNode', $node); + } + } else { + throw new \InvalidArgumentException('Node is not related to this repository'); + } + } else { + if ($direct) { + $qb->where($qb->expr()->isNull('node.'.$config['parent'])); + } + } + if (!$sortByField) { + $qb->orderBy('node.'.$config['left'], 'ASC'); + } elseif (is_array($sortByField)) { + foreach ($sortByField as $key => $field) { + $fieldDirection = is_array($direction) ? ($direction[$key] ?? 'asc') : $direction; + if (($meta->hasField($field) || $meta->isSingleValuedAssociation($field)) && in_array(strtolower($fieldDirection), ['asc', 'desc'], true)) { + $qb->addOrderBy('node.'.$field, $fieldDirection); + } else { + throw new InvalidArgumentException(sprintf('Invalid sort options specified: field - %s, direction - %s', $field, $fieldDirection)); + } + } + } else { + if (($meta->hasField($sortByField) || $meta->isSingleValuedAssociation($sortByField)) && in_array(strtolower($direction), ['asc', 'desc'], true)) { + $qb->orderBy('node.'.$sortByField, $direction); + } else { + throw new InvalidArgumentException(sprintf('Invalid sort options specified: field - %s, direction - %s', $sortByField, $direction)); + } + } + + return $qb; + } + + /** + * @param object|null $node if null, all tree nodes will be taken + * @param bool $direct true to take only direct children + * @param string|string[]|null $sortByField Field name or array of fields names to sort by + * @param string|string[] $direction Sort order ('asc'|'desc'|'ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements + * @param bool $includeNode Include the root node in results? + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC'|array $direction + * + * @return Query Query object + */ + public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); + } + + /** + * @param object|null $node The object to fetch children for; if null, all nodes will be retrieved + * @param bool $direct Flag indicating whether only direct children should be retrieved + * @param string|string[]|null $sortByField Field name or array of fields names to sort by + * @param string|string[] $direction Sort order ('asc'|'desc'|'ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements + * @param bool $includeNode Flag indicating whether the given node should be included in the results + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC'|array $direction + * + * @return array List of children + */ + public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode)->getResult(); + } + + public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode); + } + + public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode); + } + + /** + * @return array + */ + public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) + { + return $this->children($node, $direct, $sortByField, $direction, $includeNode); + } + + /** + * Get tree leafs query builder + * + * @param object $root root node in case of root tree is required + * @param string $sortByField field name to sort by + * @param string $direction sort direction : "ASC" or "DESC" + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC' $direction + * + * @throws InvalidArgumentException if input is not valid + * + * @return QueryBuilder + */ + public function getLeafsQueryBuilder($root = null, $sortByField = null, $direction = 'ASC') + { + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + + if (isset($config['root']) && null === $root) { + throw new InvalidArgumentException('If tree has root, getLeafs method requires any node of this tree'); + } + + $qb = $this->getQueryBuilder(); + $qb->select('node') + ->from($config['useObjectClass'], 'node') + ->where($qb->expr()->eq('node.'.$config['right'], '1 + node.'.$config['left'])) + ; + if (isset($config['root'])) { + if (is_a($root, $meta->getName())) { + $wrapped = new EntityWrapper($root, $this->getEntityManager()); + $rootId = $wrapped->getPropertyValue($config['root']); + if (!$rootId) { + throw new InvalidArgumentException('Root node must be managed'); + } + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $rootId); + } else { + throw new InvalidArgumentException('Node is not related to this repository'); + } + } + if (!$sortByField) { + if (isset($config['root'])) { + $qb->addOrderBy('node.'.$config['root'], 'ASC'); + } + $qb->addOrderBy('node.'.$config['left'], 'ASC'); + } else { + if ($meta->hasField($sortByField) && in_array(strtolower($direction), ['asc', 'desc'], true)) { + $qb->orderBy('node.'.$sortByField, $direction); + } else { + throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}"); + } + } + + return $qb; + } + + /** + * Get tree leafs query + * + * @param object $root root node in case of root tree is required + * @param string $sortByField field name to sort by + * @param string $direction sort direction : "ASC" or "DESC" + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC' $direction + * + * @return Query + */ + public function getLeafsQuery($root = null, $sortByField = null, $direction = 'ASC') + { + return $this->getLeafsQueryBuilder($root, $sortByField, $direction)->getQuery(); + } + + /** + * Get list of leaf nodes of the tree + * + * @param object $root root node in case of root tree is required + * @param string $sortByField field name to sort by + * @param string $direction sort direction : "ASC" or "DESC" + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC' $direction + * + * @return array + */ + public function getLeafs($root = null, $sortByField = null, $direction = 'ASC') + { + return $this->getLeafsQuery($root, $sortByField, $direction)->getResult(); + } + + /** + * Get the query builder for next siblings of the given $node + * + * @param object $node + * @param bool $includeSelf include the node itself + * + * @throws InvalidArgumentException if input is invalid + * + * @return QueryBuilder + */ + public function getNextSiblingsQueryBuilder($node, $includeSelf = false) + { + $meta = $this->getClassMetadata(); + if (!is_a($node, $meta->getName())) { + throw new InvalidArgumentException('Node is not related to this repository'); + } + $wrapped = new EntityWrapper($node, $this->getEntityManager()); + if (!$wrapped->hasValidIdentifier()) { + throw new InvalidArgumentException('Node is not managed by UnitOfWork'); + } + + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $parent = $wrapped->getPropertyValue($config['parent']); + + $left = $wrapped->getPropertyValue($config['left']); + + $qb = $this->getQueryBuilder(); + $qb->select('node') + ->from($config['useObjectClass'], 'node') + ->where($includeSelf ? + $qb->expr()->gte('node.'.$config['left'], $left) : + $qb->expr()->gt('node.'.$config['left'], $left) + ) + ->orderBy("node.{$config['left']}", 'ASC') + ; + if ($parent) { + $wrappedParent = new EntityWrapper($parent, $this->getEntityManager()); + $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); + $qb->setParameter('pid', $wrappedParent->getIdentifier()); + } elseif (isset($config['root'])) { + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':root')); + $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); + $root = isset($config['rootIdentifierMethod']) ? + $node->{$config['rootIdentifierMethod']}() : + $wrapped->getPropertyValue($config['root']) + ; + $qb->setParameter('root', $root); + } else { + $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); + } + + return $qb; + } + + /** + * Get the query for next siblings of the given $node + * + * @param object $node + * @param bool $includeSelf include the node itself + * + * @return Query + */ + public function getNextSiblingsQuery($node, $includeSelf = false) + { + return $this->getNextSiblingsQueryBuilder($node, $includeSelf)->getQuery(); + } + + /** + * Find the next siblings of the given $node + * + * @param object $node + * @param bool $includeSelf include the node itself + * + * @return array + */ + public function getNextSiblings($node, $includeSelf = false) + { + return $this->getNextSiblingsQuery($node, $includeSelf)->getResult(); + } + + /** + * Get query builder for previous siblings of the given $node + * + * @param object $node + * @param bool $includeSelf include the node itself + * + * @throws InvalidArgumentException if input is invalid + * + * @return QueryBuilder + */ + public function getPrevSiblingsQueryBuilder($node, $includeSelf = false) + { + $meta = $this->getClassMetadata(); + if (!is_a($node, $meta->getName())) { + throw new InvalidArgumentException('Node is not related to this repository'); + } + $wrapped = new EntityWrapper($node, $this->getEntityManager()); + if (!$wrapped->hasValidIdentifier()) { + throw new InvalidArgumentException('Node is not managed by UnitOfWork'); + } + + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $parent = $wrapped->getPropertyValue($config['parent']); + + $left = $wrapped->getPropertyValue($config['left']); + + $qb = $this->getQueryBuilder(); + $qb->select('node') + ->from($config['useObjectClass'], 'node') + ->where($includeSelf ? + $qb->expr()->lte('node.'.$config['left'], $left) : + $qb->expr()->lt('node.'.$config['left'], $left) + ) + ->orderBy("node.{$config['left']}", 'ASC') + ; + if ($parent) { + $wrappedParent = new EntityWrapper($parent, $this->getEntityManager()); + $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); + $qb->setParameter('pid', $wrappedParent->getIdentifier()); + } elseif (isset($config['root'])) { + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':root')); + $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); + $method = $config['rootIdentifierMethod']; + $qb->setParameter('root', $node->$method()); + } else { + $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); + } + + return $qb; + } + + /** + * Get query for previous siblings of the given $node + * + * @param object $node + * @param bool $includeSelf include the node itself + * + * @throws InvalidArgumentException if input is invalid + * + * @return Query + */ + public function getPrevSiblingsQuery($node, $includeSelf = false) + { + return $this->getPrevSiblingsQueryBuilder($node, $includeSelf)->getQuery(); + } + + /** + * Find the previous siblings of the given $node + * + * @param object $node + * @param bool $includeSelf include the node itself + * + * @return array + */ + public function getPrevSiblings($node, $includeSelf = false) + { + return $this->getPrevSiblingsQuery($node, $includeSelf)->getResult(); + } + + /** + * Move the node down in the same level + * + * @param object $node + * @param int|bool $number integer - number of positions to shift + * boolean - if "true" - shift till last position + * + * @throws \RuntimeException if something fails in transaction + * + * @return bool true if shifted + */ + public function moveDown($node, $number = 1) + { + $result = false; + $meta = $this->getClassMetadata(); + if (is_a($node, $meta->getName())) { + $nextSiblings = $this->getNextSiblings($node); + if ($numSiblings = count($nextSiblings)) { + $result = true; + if (true === $number) { + $number = $numSiblings; + } elseif ($number > $numSiblings) { + $number = $numSiblings; + } + $this->listener + ->getStrategy($this->getEntityManager(), $meta->getName()) + ->updateNode($this->getEntityManager(), $node, $nextSiblings[$number - 1], Nested::NEXT_SIBLING); + } + } else { + throw new InvalidArgumentException('Node is not related to this repository'); + } + + return $result; + } + + /** + * Move the node up in the same level + * + * @param object $node + * @param int|bool $number integer - number of positions to shift + * boolean - true shift till first position + * + * @throws \RuntimeException if something fails in transaction + * + * @return bool true if shifted + */ + public function moveUp($node, $number = 1) + { + $result = false; + $meta = $this->getClassMetadata(); + if (is_a($node, $meta->getName())) { + $prevSiblings = array_reverse($this->getPrevSiblings($node)); + if ($numSiblings = count($prevSiblings)) { + $result = true; + if (true === $number) { + $number = $numSiblings; + } elseif ($number > $numSiblings) { + $number = $numSiblings; + } + $this->listener + ->getStrategy($this->getEntityManager(), $meta->getName()) + ->updateNode($this->getEntityManager(), $node, $prevSiblings[$number - 1], Nested::PREV_SIBLING); + } + } else { + throw new InvalidArgumentException('Node is not related to this repository'); + } + + return $result; + } + + /** + * UNSAFE: be sure to backup before running this method when necessary + * + * Removes given $node from the tree and reparents its descendants + * + * @param object $node + * + * @throws \RuntimeException if something fails in transaction + * + * @return void + */ + public function removeFromTree($node) + { + $meta = $this->getClassMetadata(); + if (is_a($node, $meta->getName())) { + $wrapped = new EntityWrapper($node, $this->getEntityManager()); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $right = $wrapped->getPropertyValue($config['right']); + $left = $wrapped->getPropertyValue($config['left']); + $rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null; + + // if node has no children + if ($right == $left + 1) { + $this->removeSingle($wrapped); + $this->listener + ->getStrategy($this->getEntityManager(), $meta->getName()) + ->shiftRL($this->getEntityManager(), $config['useObjectClass'], $right, -2, $rootId); + + return; // node was a leaf + } + // process updates in transaction + $this->getEntityManager()->getConnection()->beginTransaction(); + + try { + $parent = $wrapped->getPropertyValue($config['parent']); + $parentId = null; + if ($parent) { + $wrappedParent = new EntityWrapper($parent, $this->getEntityManager()); + $parentId = $wrappedParent->getIdentifier(); + } + $pk = $meta->getSingleIdentifierFieldName(); + $nodeId = $wrapped->getIdentifier(); + $shift = -1; + + // in case if root node is removed, children become roots + if (isset($config['root']) && !$parent) { + // get node's children + $qb = $this->getQueryBuilder(); + $qb->select('node.'.$pk, 'node.'.$config['left'], 'node.'.$config['right']) + ->from($config['useObjectClass'], 'node'); + + $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); + $qb->setParameter('pid', $nodeId); + $nodes = $qb->getQuery()->toIterable([], Query::HYDRATE_ARRAY); + + // go through each of the node's children + foreach ($nodes as $newRoot) { + $left = $newRoot[$config['left']]; + $right = $newRoot[$config['right']]; + $rootId = $newRoot[$pk]; + $shift = -($left - 1); + + // set the root of this child node and its children to the newly formed tree + $qb = $this->getQueryBuilder(); + $qb->update($config['useObjectClass'], 'node'); + $qb->set('node.'.$config['root'], ':rid'); + $qb->setParameter('rid', $rootId); + $qb->where($qb->expr()->eq('node.'.$config['root'], ':rpid')); + $qb->setParameter('rpid', $nodeId); + $qb->andWhere($qb->expr()->gte('node.'.$config['left'], $left)); + $qb->andWhere($qb->expr()->lte('node.'.$config['right'], $right)); + $qb->getQuery()->getSingleScalarResult(); + + // Set the parent to NULL for this child node, i.e. make it root + $qb = $this->getQueryBuilder(); + $qb->update($config['useObjectClass'], 'node'); + $qb->set('node.'.$config['parent'], ':pid'); + $qb->setParameter('pid', $parentId); + $qb->where($qb->expr()->eq('node.'.$config['parent'], ':rpid')); + $qb->setParameter('rpid', $nodeId); + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $rootId); + $qb->getQuery()->getSingleScalarResult(); + + // fix left, right and level values for the newly formed tree + $this->listener + ->getStrategy($this->getEntityManager(), $meta->getName()) + ->shiftRangeRL($this->getEntityManager(), $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, -1); + $this->listener + ->getStrategy($this->getEntityManager(), $meta->getName()) + ->shiftRL($this->getEntityManager(), $config['useObjectClass'], $right, -2, $rootId); + } + } else { + // set parent of all direct children to be the parent of the node being deleted + $qb = $this->getQueryBuilder(); + $qb->update($config['useObjectClass'], 'node'); + $qb->set('node.'.$config['parent'], ':pid'); + $qb->setParameter('pid', $parentId); + $qb->where($qb->expr()->eq('node.'.$config['parent'], ':rpid')); + $qb->setParameter('rpid', $nodeId); + if (isset($config['root'])) { + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $rootId); + } + $qb->getQuery()->getSingleScalarResult(); + + // fix left, right and level values for the node's children + $this->listener + ->getStrategy($this->getEntityManager(), $meta->getName()) + ->shiftRangeRL($this->getEntityManager(), $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, -1); + + $this->listener + ->getStrategy($this->getEntityManager(), $meta->getName()) + ->shiftRL($this->getEntityManager(), $config['useObjectClass'], $right, -2, $rootId); + } + $this->removeSingle($wrapped); + $this->getEntityManager()->getConnection()->commit(); + } catch (\Exception $e) { + $this->getEntityManager()->close(); + $this->getEntityManager()->getConnection()->rollback(); + + throw new RuntimeException('Transaction failed', $e->getCode(), $e); + } + } else { + throw new InvalidArgumentException('Node is not related to this repository'); + } + } + + /** + * Reorders $node's child nodes, + * according to the $sortByField and $direction specified + * + * @param object|null $node node from which to start reordering the tree; null will reorder everything + * @param string $sortByField field name to sort by + * @param string $direction sort direction : "ASC" or "DESC" + * @param bool $verify true to verify tree first + * @param bool $recursive true to also reorder further descendants, not just the direct children + * + * @return void + */ + public function reorder($node, $sortByField = null, $direction = 'ASC', $verify = true, $recursive = true) + { + $meta = $this->getClassMetadata(); + if (null === $node || is_a($node, $meta->getName())) { + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + if ($verify && is_array($this->verify())) { + return; + } + + $nodes = $this->children($node, true, $sortByField, $direction); + foreach ($nodes as $node) { + $wrapped = new EntityWrapper($node, $this->getEntityManager()); + $right = $wrapped->getPropertyValue($config['right']); + $left = $wrapped->getPropertyValue($config['left']); + $this->moveDown($node, true); + if ($recursive && $left != ($right - 1)) { + $this->reorder($node, $sortByField, $direction, false); + } + } + } else { + throw new InvalidArgumentException('Node is not related to this repository'); + } + } + + /** + * Reorders all nodes in the tree according to the $sortByField and $direction specified. + * + * @param string $sortByField field name to sort by + * @param string $direction sort direction : "ASC" or "DESC" + * @param bool $verify true to verify tree first + * + * @return void + */ + public function reorderAll($sortByField = null, $direction = 'ASC', $verify = true) + { + $this->reorder(null, $sortByField, $direction, $verify); + } + + /** + * Verifies that current tree is valid. + * If any error is detected it will return an array + * with a list of errors found on tree + * + * @phpstan-param array{treeRootNode?: object} $options + * + * options: + * - treeRootNode: (object) Optional tree root node to verify, if not the whole forest (only available for forests, not for single trees). + * + * @return array|bool true on success, error list on failure + */ + public function verify(/* array $options = [] */) // @phpstan-ignore-line + { + $options = func_get_args()[0] ?? []; + if (!\is_array($options)) { + throw new \TypeError('Argument 1 MUST be an array.'); + } + + $defaultOptions = [ + 'treeRootNode' => null, + ]; + $options += $defaultOptions; + + if (!$this->childCount()) { + return true; // tree is empty + } + + $errors = []; + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + if (isset($config['root'])) { + $trees = $this->getRootNodes(); + foreach ($trees as $tree) { + // if a root node is specified, verify only it + if (null !== $options['treeRootNode'] && $options['treeRootNode'] !== $tree) { + continue; + } + $this->verifyTree($errors, $tree); + } + } else { + $this->verifyTree($errors); + } + + return [] !== $errors ? $errors : true; + } + + /** + * Tries to recover the tree, avoiding entity object hydration and using DQL + * + * NOTE: DQL UPDATE statements are ported directly into a Database UPDATE statement and therefore bypass any locking + * scheme, events and do not increment the version column. Entities that are already loaded into the persistence + * context will NOT be synced with the updated database state. + * It is recommended to call EntityManager#clear() and retrieve new instances of any affected entity. + * + * @phpstan-param array{sortByField?: string, sortDirection?: string} $options + * + * options: + * - sortByField: (string) Optionally sort siblings by specified field while recovering. Defaults to null. + * - sortDirection: (string) The order to sort siblings in, when sortByField is specified ('ASC', 'DESC'). Defaults to 'ASC'. + * + * @throws ORMException + */ + public function recoverFast(array $options = []): void + { + $defaultOptions = [ + 'sortByField' => null, + 'sortDirection' => 'ASC', + ]; + $options += $defaultOptions; + + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name); + $em = $this->getEntityManager(); + + $updateQb = $em->createQueryBuilder() + ->update($meta->getName(), 'node') + ->set('node.'.$config['left'], ':left') + ->set('node.'.$config['right'], ':right') + ->where('node.id = :id'); + if (isset($config['level'])) { + $updateQb->set('node.'.$config['level'], ':level'); + } + + $doRecover = function (array $root, int &$count, int $level) use ($meta, $em, $options, $updateQb, &$doRecover): void { + $rootEntity = $em->getReference($meta->getName(), $root['node_id']); + $left = $count++; + $childrenQuery = $this->getChildrenQuery($rootEntity, true, $options['sortByField'], $options['sortDirection']); + foreach ($childrenQuery->getScalarResult() as $child) { + $doRecover($child, $count, $level + 1); + } + $right = $count++; + + $updateQb + ->setParameter('left', $left) + ->setParameter('right', $right) + ->setParameter('id', $root['node_id']) + ->setParameter('level', $level) + ->getQuery()->execute(); + }; + + // if it's a forest + if (isset($config['root'])) { + $rootNodesQuery = $this->getRootNodesQuery($options['sortByField'], $options['sortDirection']); + $roots = $rootNodesQuery->getScalarResult(); + foreach ($roots as $root) { + // reset on every root node + $count = 1; + $level = $config['level_base'] ?? 0; + $doRecover($root, $count, $level); + $em->clear(); + } + } else { + $count = 1; + $level = $config['level_base'] ?? 0; + $childrenQuery = $this->getChildrenQuery(null, true, $options['sortByField'], $options['sortDirection']); + foreach ($childrenQuery->getScalarResult() as $root) { + $doRecover($root, $count, $level); + $em->clear(); + } + } + } + + /** + * NOTE: flush your entity manager after, unless the 'flush' option has been set to true + * + * Tries to recover the tree + * + * @phpstan-param array{ + * flush?: bool, + * treeRootNode?: ?object, + * skipVerify?: bool, + * sortByField?: string, + * sortDirection?: string + * } $options + * + * options: + * - flush: (bool) Flush entity manager after each root node is recovered. Defaults to false. + * - treeRootNode: (object) Optional tree root node to recover, if not the whole forest (only available for forests, not for single trees). Defaults to null. + * - skipVerify: (bool) Whether to skip verification and recover anyway. Defaults to false. + * - sortByField: (string) Optionally sort siblings by specified field while recovering. Defaults to null. + * - sortDirection: (string) The order to sort siblings in, when sortByField is specified ('ASC', 'DESC'). Defaults to 'ASC'. + * + * @return void + */ + public function recover(/* array $options = [] */) // @phpstan-ignore-line + { + $options = func_get_args()[0] ?? []; + if (!\is_array($options)) { + throw new \TypeError('Argument 1 MUST be an array.'); + } + + $defaultOptions = [ + 'flush' => false, + 'treeRootNode' => null, + 'skipVerify' => false, + 'sortByField' => null, + 'sortDirection' => 'ASC', + ]; + $options += $defaultOptions; + + if (!$options['skipVerify'] && (true === $this->verify())) { + return; + } + + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $em = $this->getEntityManager(); + + $doRecover = function ($root, &$count, &$lvl) use ($meta, $config, $em, $options, &$doRecover) { + $left = $count++; + foreach ($this->getChildren($root, true, $options['sortByField'], $options['sortDirection']) as $child) { + $depth = ($lvl + 1); + $doRecover($child, $count, $depth); + } + $right = $count++; + + $meta->setFieldValue($root, $config['left'], $left); + $meta->setFieldValue($root, $config['right'], $right); + if (isset($config['level'])) { + $meta->setFieldValue($root, $config['level'], $lvl); + } + $em->persist($root); + }; + + // if it's a forest + if (isset($config['root'])) { + foreach ($this->getRootNodes($options['sortByField'], $options['sortDirection']) as $root) { + // if a root node is specified, recover only it + if (null !== $options['treeRootNode'] && $options['treeRootNode'] !== $root) { + continue; + } + + $count = 1; // reset on every root node + $lvl = $config['level_base'] ?? 0; + $doRecover($root, $count, $lvl); + + if ($options['flush']) { + $em->flush(); + } + } + } else { + $count = 1; + $lvl = $config['level_base'] ?? 0; + foreach ($this->getChildren(null, true, $options['sortByField'], $options['sortDirection']) as $root) { + $doRecover($root, $count, $lvl); + + if ($options['flush']) { + $em->flush(); + } + } + } + } + + public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false) + { + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + + return $this->childrenQueryBuilder( + $node, + $direct, + isset($config['root']) ? [$config['root'], $config['left']] : $config['left'], + 'ASC', + $includeNode + ); + } + + public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false) + { + return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); + } + + public function getNodesHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) + { + return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult(); + } + + /** + * Allows the following 'virtual' methods: + * - persistAsFirstChild($node) + * - persistAsFirstChildOf($node, $parent) + * - persistAsLastChild($node) + * - persistAsLastChildOf($node, $parent) + * - persistAsNextSibling($node) + * - persistAsNextSiblingOf($node, $sibling) + * - persistAsPrevSibling($node) + * - persistAsPrevSiblingOf($node, $sibling) + * Inherited virtual methods: + * - find* + * + * @param string $method + * @param array $args + * + * @phpstan-param list $args + * + * @throws \BadMethodCallException If the method called is an invalid find* or persistAs* method + * or no find* either persistAs* method at all and therefore an invalid method call + * @throws InvalidArgumentException If arguments are invalid + * + * @return mixed TreeNestedRepository if persistAs* is called + * + * @see \Doctrine\ORM\EntityRepository + */ + protected function doCallWithCompat($method, $args) + { + if ('persistAs' === substr($method, 0, 9)) { + if (!isset($args[0])) { + throw new InvalidArgumentException('Node to persist must be available as first argument.'); + } + $node = $args[0]; + $wrapped = new EntityWrapper($node, $this->getEntityManager()); + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + $position = substr($method, 9); + if ('Of' === substr($method, -2)) { + if (!isset($args[1])) { + throw new InvalidArgumentException('If "Of" is specified you must provide parent or sibling as the second argument.'); + } + $parentOrSibling = $args[1]; + if (strstr($method, 'Sibling')) { + $wrappedParentOrSibling = new EntityWrapper($parentOrSibling, $this->getEntityManager()); + $newParent = $wrappedParentOrSibling->getPropertyValue($config['parent']); + if (null === $newParent && isset($config['root'])) { + throw new UnexpectedValueException('Cannot persist sibling for a root node, tree operation is not possible'); + } + + if (!$node instanceof Node) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2547', + 'Not implementing the "%s" interface from node "%s" is deprecated since gedmo/doctrine-extensions' + .' 3.13 and will throw a "%s" error in version 4.0.', + Node::class, + \get_class($node), + \TypeError::class + ); + } + + // @todo: In the next major release, remove the previous condition and uncomment the following one. + + // if (!$node instanceof Node) { + // throw new \TypeError(\sprintf( + // 'Node MUST implement "%s" interface.', + // Node::class + // )); + // } + + // @todo: In the next major release, remove the `method_exists()` condition and left the `else` branch. + if (!method_exists($node, 'setSibling')) { + $node->sibling = $parentOrSibling; + } else { + $node->setSibling($parentOrSibling); + } + $parentOrSibling = $newParent; + } + $wrapped->setPropertyValue($config['parent'], $parentOrSibling); + $position = substr($position, 0, -2); + } + $wrapped->setPropertyValue($config['left'], 0); // simulate changeset + $oid = spl_object_id($node); + $this->listener + ->getStrategy($this->getEntityManager(), $meta->getName()) + ->setNodePosition($oid, $position) + ; + + $this->getEntityManager()->persist($node); + + return $this; + } + + return parent::__call($method, $args); + } + + protected function validate() + { + return Strategy::NESTED === $this->listener->getStrategy($this->getEntityManager(), $this->getClassMetadata()->name)->getName(); + } + + /** + * Collect errors on given tree if + * where are any + * + * @param array $errors + */ + private function verifyTree(array &$errors, ?object $root = null): void + { + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + + $identifier = $meta->getSingleIdentifierFieldName(); + if ($root && isset($config['root'])) { + $rootId = $meta->getFieldValue($root, $config['root']); + if (is_object($rootId)) { + $rootId = $meta->getFieldValue($rootId, $identifier); + } + } else { + $rootId = null; + } + + $qb = $this->getQueryBuilder(); + $qb->select($qb->expr()->min('node.'.$config['left'])) + ->from($config['useObjectClass'], 'node') + ; + if (isset($config['root'])) { + $qb->where($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $rootId); + } + $min = (int) $qb->getQuery()->getSingleScalarResult(); + $edge = $this->listener->getStrategy($this->getEntityManager(), $meta->getName())->max($this->getEntityManager(), $config['useObjectClass'], $rootId); + // check duplicate right and left values + for ($i = $min; $i <= $edge; ++$i) { + $qb = $this->getQueryBuilder(); + $qb->select($qb->expr()->count('node.'.$identifier)) + ->from($config['useObjectClass'], 'node') + ->where($qb->expr()->orX( + $qb->expr()->eq('node.'.$config['left'], $i), + $qb->expr()->eq('node.'.$config['right'], $i) + )) + ; + if (isset($config['root'])) { + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $rootId); + } + $count = (int) $qb->getQuery()->getSingleScalarResult(); + if (1 !== $count) { + if (0 === $count) { + $errors[] = "index [{$i}], missing".($root ? ' on tree root: '.$rootId : ''); + } else { + $errors[] = "index [{$i}], duplicate".($root ? ' on tree root: '.$rootId : ''); + } + } + } + // check for missing parents + $qb = $this->getQueryBuilder(); + $qb->select('node') + ->from($config['useObjectClass'], 'node') + ->leftJoin('node.'.$config['parent'], 'parent') + ->where($qb->expr()->isNotNull('node.'.$config['parent'])) + ->andWhere($qb->expr()->isNull('parent.'.$identifier)) + ; + if (isset($config['root'])) { + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $rootId); + } + + $areMissingParents = false; + + foreach ($qb->getQuery()->toIterable([], Query::HYDRATE_ARRAY) as $node) { + $areMissingParents = true; + $errors[] = "node [{$node[$identifier]}] has missing parent".($root ? ' on tree root: '.$rootId : ''); + } + + // loading broken relation can cause infinite loop + if ($areMissingParents) { + return; + } + + // check for nodes that have a right value lower than the left + $qb = $this->getQueryBuilder(); + $qb->select('node') + ->from($config['useObjectClass'], 'node') + ->where($qb->expr()->lt('node.'.$config['right'], 'node.'.$config['left'])) + ; + if (isset($config['root'])) { + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $rootId); + } + $result = $qb->getQuery() + ->setMaxResults(1) + ->getResult(Query::HYDRATE_ARRAY); + $node = [] !== $result ? array_shift($result) : []; + + if ([] !== $node) { + $id = $node[$identifier]; + $errors[] = "node [{$id}], left is greater than right".($root ? ' on tree root: '.$rootId : ''); + } + + $qb = $this->getQueryBuilder(); + $qb->select('node') + ->from($config['useObjectClass'], 'node') + ; + if (isset($config['root'])) { + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $rootId); + } + + foreach ($qb->getQuery()->toIterable() as $node) { + $right = $meta->getFieldValue($node, $config['right']); + $left = $meta->getFieldValue($node, $config['left']); + $id = $meta->getFieldValue($node, $identifier); + $parent = $meta->getFieldValue($node, $config['parent']); + if (!$right || !$left) { + $errors[] = "node [{$id}] has invalid left or right values"; + } elseif ($right == $left) { + $errors[] = "node [{$id}] has identical left and right values"; + } elseif ($parent) { + if ($parent instanceof Proxy && !$parent->__isInitialized()) { + $this->getEntityManager()->refresh($parent); + } + $parentRight = $meta->getFieldValue($parent, $config['right']); + $parentLeft = $meta->getFieldValue($parent, $config['left']); + $parentId = $meta->getFieldValue($parent, $identifier); + if ($left < $parentLeft) { + $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value"; + } elseif ($right > $parentRight) { + $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value"; + } + // check that level of node is exactly after its parent's level + if (isset($config['level'])) { + $parentLevel = $meta->getFieldValue($parent, $config['level']); + $level = $meta->getFieldValue($node, $config['level']); + if ($level !== $parentLevel + 1) { + $errors[] = "node [{$id}] should be on the level right after its parent`s [{$parentId}] level"; + } + } + } else { + // check that level of the root node is the base level defined + if (isset($config['level'])) { + $baseLevel = $config['level_base'] ?? 0; + $level = $meta->getFieldValue($node, $config['level']); + if ($level !== $baseLevel) { + $errors[] = "node [{$id}] should be on level {$baseLevel}, not {$level}"; + } + } + + // get number of parents of node, based on left and right values + $qb = $this->getQueryBuilder(); + $qb->select($qb->expr()->count('node.'.$identifier)) + ->from($config['useObjectClass'], 'node') + ->where($qb->expr()->lt('node.'.$config['left'], $left)) + ->andWhere($qb->expr()->gt('node.'.$config['right'], $right)) + ; + if (isset($config['root'])) { + $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); + $qb->setParameter('rid', $rootId); + } + if ($count = (int) $qb->getQuery()->getSingleScalarResult()) { + $errors[] = "node [{$id}] parent field is blank, but it has a parent"; + } + } + } + } + + /** + * Removes single node without touching children + * + * @param EntityWrapper $wrapped + * + * @internal + */ + private function removeSingle(EntityWrapper $wrapped): void + { + $meta = $this->getClassMetadata(); + $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName()); + + $pk = $meta->getSingleIdentifierFieldName(); + $nodeId = $wrapped->getIdentifier(); + // prevent from deleting whole branch + $qb = $this->getQueryBuilder(); + $qb->update($config['useObjectClass'], 'node') + ->set('node.'.$config['left'], 0) + ->set('node.'.$config['right'], 0); + + $qb->andWhere($qb->expr()->eq('node.'.$pk, ':id')); + $qb->setParameter('id', $nodeId); + $qb->getQuery()->getSingleScalarResult(); + + // remove the node from database + $qb = $this->getQueryBuilder(); + $qb->delete($config['useObjectClass'], 'node'); + $qb->andWhere($qb->expr()->eq('node.'.$pk, ':id')); + $qb->setParameter('id', $nodeId); + $qb->getQuery()->getSingleScalarResult(); + + // remove from identity map + $this->getEntityManager()->getUnitOfWork()->removeFromIdentityMap($wrapped->getObject()); + } +} diff --git a/src/Tree/Hydrator/ORM/TreeObjectHydrator.php b/src/Tree/Hydrator/ORM/TreeObjectHydrator.php new file mode 100644 index 0000000000..42f2d6d849 --- /dev/null +++ b/src/Tree/Hydrator/ORM/TreeObjectHydrator.php @@ -0,0 +1,305 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree\Hydrator\ORM; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Internal\Hydration\ObjectHydrator; +use Doctrine\ORM\PersistentCollection; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Tool\ORM\Hydration\EntityManagerRetriever; +use Gedmo\Tool\ORM\Hydration\HydratorCompat; +use Gedmo\Tree\TreeListener; + +/** + * Automatically maps the parent and children properties of Tree nodes + * + * @author Ilija Tovilo + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class TreeObjectHydrator extends ObjectHydrator +{ + use EntityManagerRetriever; + use HydratorCompat; + + private const NO_PARENT_ID = '__no-parent__'; + + /** + * @var array + */ + private $config = []; + + /** + * @var string + */ + private $idField; + + /** + * @var string + */ + private $parentField; + + /** + * @var string + */ + private $childrenField; + + /** + * @param object $object + * @param string $property + * @param mixed $value + * + * @return void + */ + public function setPropertyValue($object, $property, $value) + { + $meta = $this->getEntityManager()->getClassMetadata(get_class($object)); + $meta->setFieldValue($object, $property, $value); + } + + /** + * We hook into the `hydrateAllData` to map the children collection of the entity + * + * @return array + */ + protected function doHydrateAllData() + { + $data = parent::hydrateAllData(); + + if ([] === $data) { + return $data; + } + + $listener = $this->getTreeListener($this->getEntityManager()); + $entityClass = $this->getEntityClassFromHydratedData($data); + $this->config = $listener->getConfiguration($this->getEntityManager(), $entityClass); + $this->idField = $this->getIdField($entityClass); + $this->parentField = $this->getParentField(); + $this->childrenField = $this->getChildrenField($entityClass); + + $childrenHashmap = $this->buildChildrenHashmap($data); + $this->populateChildrenArray($data, $childrenHashmap); + + // Only return root elements or elements who's parents haven't been fetched + // The sub-nodes will be accessible via the `children` property + return $this->getRootNodes($data); + } + + /** + * Creates a hashmap to quickly find the children of a node + * + * ``` + * [parentId => [child1, child2, ...], ...] + * ``` + * + * @param array $nodes + * + * @return array> + */ + protected function buildChildrenHashmap($nodes) + { + $r = []; + + foreach ($nodes as $node) { + $parentProxy = $this->getPropertyValue($node, $this->config['parent']); + $parentId = self::NO_PARENT_ID; + + if (null !== $parentProxy) { + $parentId = $this->getPropertyValue($parentProxy, $this->idField); + } + + $r[$parentId][] = $node; + } + + return $r; + } + + /** + * @param array $nodes + * @param array> $childrenHashmap + * + * @return void + */ + protected function populateChildrenArray($nodes, $childrenHashmap) + { + foreach ($nodes as $node) { + $nodeId = $this->getPropertyValue($node, $this->idField); + $childrenCollection = $this->getPropertyValue($node, $this->childrenField); + + if (null === $childrenCollection) { + $childrenCollection = new ArrayCollection(); + $this->setPropertyValue($node, $this->childrenField, $childrenCollection); + } + + // Initialize all the children collections in order to avoid "SELECT" queries. + if ($childrenCollection instanceof PersistentCollection && !$childrenCollection->isInitialized()) { + $childrenCollection->setInitialized(true); + } + + if (!isset($childrenHashmap[$nodeId])) { + continue; + } + + $childrenCollection->clear(); + + foreach ($childrenHashmap[$nodeId] as $child) { + $childrenCollection->add($child); + } + } + } + + /** + * @param array $nodes + * + * @return array + */ + protected function getRootNodes($nodes) + { + $idHashmap = $this->buildIdHashmap($nodes); + $rootNodes = []; + + foreach ($nodes as $node) { + $parentProxy = $this->getPropertyValue($node, $this->config['parent']); + $parentId = self::NO_PARENT_ID; + + if (null !== $parentProxy) { + $parentId = $this->getPropertyValue($parentProxy, $this->idField); + } + + if (self::NO_PARENT_ID === $parentId || !array_key_exists($parentId, $idHashmap)) { + $rootNodes[] = $node; + } + } + + return $rootNodes; + } + + /** + * Creates a hashmap of all nodes returned in the query + * + * ``` + * [node1.id => true, node2.id => true, ...] + * ``` + * + * @param array $nodes + * + * @return array + */ + protected function buildIdHashmap(array $nodes) + { + $ids = []; + + foreach ($nodes as $node) { + $id = $this->getPropertyValue($node, $this->idField); + $ids[$id] = true; + } + + return $ids; + } + + /** + * @param string $entityClass + * + * @phpstan-param class-string $entityClass + * + * @return string + */ + protected function getIdField($entityClass) + { + $meta = $this->getClassMetadata($entityClass); + + return $meta->getSingleIdentifierFieldName(); + } + + /** + * @return string + */ + protected function getParentField() + { + if (!isset($this->config['parent'])) { + throw new InvalidMappingException('The `parent` property is required for the TreeHydrator to work'); + } + + return $this->config['parent']; + } + + /** + * @param string $entityClass + * + * @phpstan-param class-string $entityClass + * + * @return string + */ + protected function getChildrenField($entityClass) + { + $meta = $this->getClassMetadata($entityClass); + + foreach ($meta->getReflectionProperties() as $property) { + // Skip properties that have no association + if (!$meta->hasAssociation($property->getName())) { + continue; + } + + $associationMapping = $meta->getAssociationMapping($property->getName()); + + // Make sure the association is mapped by the parent property + if ($associationMapping['mappedBy'] !== $this->parentField) { + continue; + } + + return $associationMapping['fieldName']; + } + + throw new InvalidMappingException('The children property could not found. It is identified through the `mappedBy` annotation to your parent property.'); + } + + /** + * @return TreeListener + */ + protected function getTreeListener(EntityManagerInterface $em) + { + foreach ($em->getEventManager()->getAllListeners() as $listeners) { + foreach ($listeners as $listener) { + if ($listener instanceof TreeListener) { + return $listener; + } + } + } + + throw new InvalidMappingException('Tree listener was not found on your entity manager, it must be hooked into the event manager'); + } + + /** + * @param array $data + * + * @return string + */ + protected function getEntityClassFromHydratedData($data) + { + $firstMappedEntity = array_values($data); + $firstMappedEntity = $firstMappedEntity[0]; + + return $this->getEntityManager()->getClassMetadata(get_class($firstMappedEntity))->rootEntityName; + } + + /** + * @param object $object + * @param string $property + * + * @return mixed + */ + protected function getPropertyValue($object, $property) + { + $meta = $this->getEntityManager()->getClassMetadata(get_class($object)); + + return $meta->getFieldValue($object, $property); + } +} diff --git a/src/Tree/Mapping/Driver/Annotation.php b/src/Tree/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..d37d20c7d5 --- /dev/null +++ b/src/Tree/Mapping/Driver/Annotation.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the tree extension which reads extended metadata from annotations on class which is part of a tree. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/lib/Gedmo/Tree/Mapping/Driver/Annotation.php b/src/Tree/Mapping/Driver/Attribute.php similarity index 56% rename from lib/Gedmo/Tree/Mapping/Driver/Annotation.php rename to src/Tree/Mapping/Driver/Attribute.php index 304f91881c..e8547aac6a 100644 --- a/lib/Gedmo/Tree/Mapping/Driver/Annotation.php +++ b/src/Tree/Mapping/Driver/Attribute.php @@ -1,246 +1,316 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Mapping\Driver; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Tree; +use Gedmo\Mapping\Annotation\TreeClosure; +use Gedmo\Mapping\Annotation\TreeLeft; +use Gedmo\Mapping\Annotation\TreeLevel; +use Gedmo\Mapping\Annotation\TreeLockTime; +use Gedmo\Mapping\Annotation\TreeParent; +use Gedmo\Mapping\Annotation\TreePath; +use Gedmo\Mapping\Annotation\TreePathHash; +use Gedmo\Mapping\Annotation\TreePathSource; +use Gedmo\Mapping\Annotation\TreeRight; +use Gedmo\Mapping\Annotation\TreeRoot; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; use Gedmo\Tree\Mapping\Validator; /** - * This is an annotation mapping driver for Tree - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Tree - * extension. + * Mapping driver for the tree extension which reads extended metadata from attributes on class which is part of a tree. * * @author Gediminas Morkevicius * @author + * @author Kevin Mian Kraiker + * * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ -class Annotation extends AbstractAnnotationDriver +class Attribute extends AbstractAnnotationDriver { /** - * Annotation to define the tree type + * Mapping object to configure the type of tree. */ - const TREE = 'Gedmo\\Mapping\\Annotation\\Tree'; + public const TREE = Tree::class; /** - * Annotation to mark field as one which will store left value + * Mapping object to mark the field which will store the left value of a tree node. */ - const LEFT = 'Gedmo\\Mapping\\Annotation\\TreeLeft'; + public const LEFT = TreeLeft::class; /** - * Annotation to mark field as one which will store right value + * Mapping object to mark the field which will store the right value of a tree node. */ - const RIGHT = 'Gedmo\\Mapping\\Annotation\\TreeRight'; + public const RIGHT = TreeRight::class; /** - * Annotation to mark relative parent field + * Mapping object to mark the field which will store the reference to the parent of a tree node. */ - const PARENT = 'Gedmo\\Mapping\\Annotation\\TreeParent'; + public const PARENT = TreeParent::class; /** - * Annotation to mark node level + * Mapping object to mark the field which will store the level of a tree node. */ - const LEVEL = 'Gedmo\\Mapping\\Annotation\\TreeLevel'; + public const LEVEL = TreeLevel::class; /** - * Annotation to mark field as tree root + * Mapping object to mark the field which will store the reference to the root of a tree node. */ - const ROOT = 'Gedmo\\Mapping\\Annotation\\TreeRoot'; + public const ROOT = TreeRoot::class; /** - * Annotation to specify closure tree class + * Mapping object to configure a closure tree object. */ - const CLOSURE = 'Gedmo\\Mapping\\Annotation\\TreeClosure'; + public const CLOSURE = TreeClosure::class; /** - * Annotation to specify path class + * Mapping object to configure a tree path field. */ - const PATH = 'Gedmo\\Mapping\\Annotation\\TreePath'; + public const PATH = TreePath::class; /** - * Annotation to specify path source class + * Mapping object to specify the source for a tree path. */ - const PATH_SOURCE = 'Gedmo\\Mapping\\Annotation\\TreePathSource'; + public const PATH_SOURCE = TreePathSource::class; /** - * Annotation to specify path hash class + * Mapping object to configure the hash for a tree path. */ - const PATH_HASH = 'Gedmo\\Mapping\\Annotation\\TreePathHash'; + public const PATH_HASH = TreePathHash::class; /** - * Annotation to mark the field to be used to hold the lock time + * Mapping object to configure the lock time for a tree. */ - const LOCK_TIME = 'Gedmo\\Mapping\\Annotation\\TreeLockTime'; + public const LOCK_TIME = TreeLockTime::class; /** * List of tree strategies available * - * @var array + * @var string[] */ - protected $strategies = array( + protected $strategies = [ 'nested', 'closure', 'materializedPath', - ); + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { $validator = new Validator(); $class = $this->getMetaReflectionClass($meta); + // class annotations if ($annot = $this->reader->getClassAnnotation($class, self::TREE)) { - if (!in_array($annot->type, $this->strategies)) { + \assert($annot instanceof Tree); + + if (!in_array($annot->type, $this->strategies, true)) { throw new InvalidMappingException("Tree type: {$annot->type} is not available."); } + $config['strategy'] = $annot->type; $config['activate_locking'] = $annot->activateLocking; $config['locking_timeout'] = (int) $annot->lockingTimeout; if ($config['locking_timeout'] < 1) { - throw new InvalidMappingException("Tree Locking Timeout must be at least of 1 second."); + throw new InvalidMappingException('Tree Locking Timeout must be at least of 1 second.'); } } + if ($annot = $this->reader->getClassAnnotation($class, self::CLOSURE)) { + \assert($annot instanceof TreeClosure); + if (!$cl = $this->getRelatedClassName($meta, $annot->class)) { throw new InvalidMappingException("Tree closure class: {$annot->class} does not exist."); } + $config['closure'] = $cl; } // property annotations foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() || - $meta->isInheritedField($property->name) || - isset($meta->associationMappings[$property->name]['inherited']) + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) ) { continue; } + // left if ($this->reader->getPropertyAnnotation($property, self::LEFT)) { $field = $property->getName(); + if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'left' - [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find 'left' - [{$field}] as mapped property in entity - {$meta->getName()}"); } + if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } + $config['left'] = $field; } + // right if ($this->reader->getPropertyAnnotation($property, self::RIGHT)) { $field = $property->getName(); + if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'right' - [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find 'right' - [{$field}] as mapped property in entity - {$meta->getName()}"); } + if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } + $config['right'] = $field; } + // ancestor/parent if ($this->reader->getPropertyAnnotation($property, self::PARENT)) { $field = $property->getName(); + if (!$meta->isSingleValuedAssociation($field)) { - throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}"); + throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } + $config['parent'] = $field; } + // root - if ($this->reader->getPropertyAnnotation($property, self::ROOT)) { + if ($annot = $this->reader->getPropertyAnnotation($property, self::ROOT)) { + \assert($annot instanceof TreeRoot); + $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'root' - [{$field}] as mapped property in entity - {$meta->name}"); - } - if (!$validator->isValidFieldForRoot($meta, $field)) { - throw new InvalidMappingException("Tree root field - [{$field}] type is not valid and must be any of the 'integer' types or 'string' in class - {$meta->name}"); + if (!$meta->isSingleValuedAssociation($field)) { + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find 'root' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$validator->isValidFieldForRoot($meta, $field)) { + throw new InvalidMappingException("Tree root field should be either a literal property ('integer' types or 'string') or a many-to-one association through root field - [{$field}] in class - {$meta->getName()}"); + } } + + $config['rootIdentifierMethod'] = $annot->identifierMethod; $config['root'] = $field; } + // level - if ($this->reader->getPropertyAnnotation($property, self::LEVEL)) { + if ($annot = $this->reader->getPropertyAnnotation($property, self::LEVEL)) { + \assert($annot instanceof TreeLevel); + $field = $property->getName(); + if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'level' - [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find 'level' - [{$field}] as mapped property in entity - {$meta->getName()}"); } + if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } + $config['level'] = $field; + $config['level_base'] = (int) $annot->base; } + // path - if ($pathAnnotation = $this->reader->getPropertyAnnotation($property, self::PATH)) { + if ($annot = $this->reader->getPropertyAnnotation($property, self::PATH)) { + \assert($annot instanceof TreePath); + $field = $property->getName(); + if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'path' - [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find 'path' - [{$field}] as mapped property in entity - {$meta->getName()}"); } + if (!$validator->isValidFieldForPath($meta, $field)) { - throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->name}"); + throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->getName()}"); } - if (strlen($pathAnnotation->separator) > 1) { - throw new InvalidMappingException("Tree Path field - [{$field}] Separator {$pathAnnotation->separator} is invalid. It must be only one character long."); + + if (strlen($annot->separator) > 1) { + throw new InvalidMappingException("Tree Path field - [{$field}] Separator {$annot->separator} is invalid. It must be only one character long."); } + $config['path'] = $field; - $config['path_separator'] = $pathAnnotation->separator; - $config['path_append_id'] = $pathAnnotation->appendId; - $config['path_starts_with_separator'] = $pathAnnotation->startsWithSeparator; - $config['path_ends_with_separator'] = $pathAnnotation->endsWithSeparator; + $config['path_separator'] = $annot->separator; + $config['path_append_id'] = $annot->appendId; + $config['path_starts_with_separator'] = $annot->startsWithSeparator; + $config['path_ends_with_separator'] = $annot->endsWithSeparator; } + // path source - if ($this->reader->getPropertyAnnotation($property, self::PATH_SOURCE)) { + if (null !== $this->reader->getPropertyAnnotation($property, self::PATH_SOURCE)) { $field = $property->getName(); + if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'path_source' - [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find 'path_source' - [{$field}] as mapped property in entity - {$meta->getName()}"); } + if (!$validator->isValidFieldForPathSource($meta, $field)) { - throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->name}"); + throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->getName()}"); } + $config['path_source'] = $field; } - // path hash - if ($this->reader->getPropertyAnnotation($property, self::PATH_HASH)) { + // path hash + if (null !== $this->reader->getPropertyAnnotation($property, self::PATH_HASH)) { $field = $property->getName(); + if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'path_hash' - [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find 'path_hash' - [{$field}] as mapped property in entity - {$meta->getName()}"); } + if (!$validator->isValidFieldForPathHash($meta, $field)) { - throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->name}"); + throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->getName()}"); } + $config['path_hash'] = $field; } - // lock time - if ($this->reader->getPropertyAnnotation($property, self::LOCK_TIME)) { + // lock time + if (null !== $this->reader->getPropertyAnnotation($property, self::LOCK_TIME)) { $field = $property->getName(); + if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'lock_time' - [{$field}] as mapped property in entity - {$meta->name}"); + throw new InvalidMappingException("Unable to find 'lock_time' - [{$field}] as mapped property in entity - {$meta->getName()}"); } + if (!$validator->isValidFieldForLockTime($meta, $field)) { - throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->name}"); + throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->getName()}"); } + $config['lock_time'] = $field; } } if (isset($config['activate_locking']) && $config['activate_locking'] && !isset($config['lock_time'])) { - throw new InvalidMappingException("You need to map a date field as the tree lock time field to activate locking support."); + throw new InvalidMappingException('You need to map a date field as the tree lock time field to activate locking support.'); } if (!$meta->isMappedSuperclass && $config) { if (isset($config['strategy'])) { - if (is_array($meta->identifier) && count($meta->identifier) > 1) { - throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->name}"); + if (is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->getName()}"); } + $method = 'validate'.ucfirst($config['strategy']).'TreeMetadata'; $validator->$method($meta, $config); } else { - throw new InvalidMappingException("Cannot find Tree type for class: {$meta->name}"); + throw new InvalidMappingException("Cannot find Tree type for class: {$meta->getName()}"); } } + + return $config; } } diff --git a/lib/Gedmo/Tree/Mapping/Driver/Xml.php b/src/Tree/Mapping/Driver/Xml.php similarity index 64% rename from lib/Gedmo/Tree/Mapping/Driver/Xml.php rename to src/Tree/Mapping/Driver/Xml.php index 406618fa6b..383af002ae 100644 --- a/lib/Gedmo/Tree/Mapping/Driver/Xml.php +++ b/src/Tree/Mapping/Driver/Xml.php @@ -1,9 +1,16 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Mapping\Driver; -use Gedmo\Mapping\Driver\Xml as BaseXml; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver\Xml as BaseXml; use Gedmo\Tree\Mapping\Validator; /** @@ -15,47 +22,45 @@ * @author Gustavo Falco * @author Gediminas Morkevicius * @author Miha Vrhovnik - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ class Xml extends BaseXml { /** * List of tree strategies available * - * @var array + * @var string[] */ - private $strategies = array( + private array $strategies = [ 'nested', 'closure', 'materializedPath', - ); + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { /** - * @var \SimpleXmlElement $xml + * @var \SimpleXmlElement */ - $xml = $this->_getMapping($meta->name); + $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); $validator = new Validator(); if (isset($xml->tree) && $this->_isAttributeSet($xml->tree, 'type')) { $strategy = $this->_getAttribute($xml->tree, 'type'); - if (!in_array($strategy, $this->strategies)) { + if (!in_array($strategy, $this->strategies, true)) { throw new InvalidMappingException("Tree type: $strategy is not available."); } $config['strategy'] = $strategy; - $config['activate_locking'] = $this->_getAttribute($xml->tree, 'activate-locking') === 'true' ? true : false; + $config['activate_locking'] = $this->_isAttributeSet($xml->tree, 'activate-locking') && $this->_getBooleanAttribute($xml->tree, 'activate-locking'); if ($lockingTimeout = $this->_getAttribute($xml->tree, 'locking-timeout')) { $config['locking_timeout'] = (int) $lockingTimeout; if ($config['locking_timeout'] < 1) { - throw new InvalidMappingException("Tree Locking Timeout must be at least of 1 second."); + throw new InvalidMappingException('Tree Locking Timeout must be at least of 1 second.'); } } else { $config['locking_timeout'] = 3; @@ -76,27 +81,27 @@ public function readExtendedMetadata($meta, array &$config) $field = $this->_getAttribute($mappingDoctrine, 'name'); if (isset($mapping->{'tree-left'})) { if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } $config['left'] = $field; } elseif (isset($mapping->{'tree-right'})) { if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } $config['right'] = $field; } elseif (isset($mapping->{'tree-root'})) { if (!$validator->isValidFieldForRoot($meta, $field)) { - throw new InvalidMappingException("Tree root field - [{$field}] type is not valid and must be any of the 'integer' types or 'string' in class - {$meta->name}"); + throw new InvalidMappingException("Tree root field - [{$field}] type is not valid and must be any of the 'integer' types or 'string' in class - {$meta->getName()}"); } $config['root'] = $field; } elseif (isset($mapping->{'tree-level'})) { if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } $config['level'] = $field; } elseif (isset($mapping->{'tree-path'})) { if (!$validator->isValidFieldForPath($meta, $field)) { - throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->name}"); + throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->getName()}"); } $separator = $this->_getAttribute($mapping->{'tree-path'}, 'separator'); @@ -105,29 +110,9 @@ public function readExtendedMetadata($meta, array &$config) throw new InvalidMappingException("Tree Path field - [{$field}] Separator {$separator} is invalid. It must be only one character long."); } - $appendId = $this->_getAttribute($mapping->{'tree-path'}, 'append_id'); - - if (!$appendId) { - $appendId = true; - } else { - $appendId = strtolower($appendId) == 'false' ? false : true; - } - - $startsWithSeparator = $this->_getAttribute($mapping->{'tree-path'}, 'starts_with_separator'); - - if (!$startsWithSeparator) { - $startsWithSeparator = false; - } else { - $startsWithSeparator = strtolower($startsWithSeparator) == 'false' ? false : true; - } - - $endsWithSeparator = $this->_getAttribute($mapping->{'tree-path'}, 'ends_with_separator'); - - if (!$endsWithSeparator) { - $endsWithSeparator = true; - } else { - $endsWithSeparator = strtolower($endsWithSeparator) == 'false' ? false : true; - } + $appendId = $this->_isAttributeSet($mapping->{'tree-path'}, 'append_id') ? $this->_getBooleanAttribute($mapping->{'tree-path'}, 'append_id') : null; + $startsWithSeparator = $this->_isAttributeSet($mapping->{'tree-path'}, 'starts_with_separator') && $this->_getBooleanAttribute($mapping->{'tree-path'}, 'starts_with_separator'); + $endsWithSeparator = !$this->_isAttributeSet($mapping->{'tree-path'}, 'ends_with_separator') || $this->_getBooleanAttribute($mapping->{'tree-path'}, 'ends_with_separator'); $config['path'] = $field; $config['path_separator'] = $separator; @@ -136,12 +121,17 @@ public function readExtendedMetadata($meta, array &$config) $config['path_ends_with_separator'] = $endsWithSeparator; } elseif (isset($mapping->{'tree-path-source'})) { if (!$validator->isValidFieldForPathSource($meta, $field)) { - throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->name}"); + throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->getName()}"); } $config['path_source'] = $field; + } elseif (isset($mapping->{'tree-path-hash'})) { + if (!$validator->isValidFieldForPathSource($meta, $field)) { + throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid and must be 'string' in class - {$meta->getName()}"); + } + $config['path_hash'] = $field; } elseif (isset($mapping->{'tree-lock-time'})) { if (!$validator->isValidFieldForLockTime($meta, $field)) { - throw new InvalidMappingException("Tree LockTime field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->name}"); + throw new InvalidMappingException("Tree LockTime field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->getName()}"); } $config['lock_time'] = $field; } @@ -149,89 +139,121 @@ public function readExtendedMetadata($meta, array &$config) } if (isset($config['activate_locking']) && $config['activate_locking'] && !isset($config['lock_time'])) { - throw new InvalidMappingException("You need to map a date field as the tree lock time field to activate locking support."); + throw new InvalidMappingException('You need to map a date field as the tree lock time field to activate locking support.'); } - if ($xmlDoctrine->getName() == 'mapped-superclass') { + if ('mapped-superclass' === $xmlDoctrine->getName()) { if (isset($xmlDoctrine->{'many-to-one'})) { foreach ($xmlDoctrine->{'many-to-one'} as $manyToOneMapping) { /** - * @var \SimpleXMLElement $manyToOneMapping + * @var \SimpleXMLElement */ $manyToOneMappingDoctrine = $manyToOneMapping; $manyToOneMapping = $manyToOneMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($manyToOneMapping->{'tree-parent'})) { $field = $this->_getAttribute($manyToOneMappingDoctrine, 'field'); - $targetEntity = $meta->associationMappings[$field]['targetEntity']; + $targetEntity = $meta->getAssociationTargetClass($field); if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) { - throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}"); + throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } $config['parent'] = $field; } + if (isset($manyToOneMapping->{'tree-root'})) { + $field = $this->_getAttribute($manyToOneMappingDoctrine, 'field'); + $targetEntity = $meta->getAssociationTargetClass($field); + if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) { + throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->getName()}"); + } + $config['root'] = $field; + } } } elseif (isset($xmlDoctrine->{'reference-one'})) { foreach ($xmlDoctrine->{'reference-one'} as $referenceOneMapping) { /** - * @var \SimpleXMLElement $referenceOneMapping + * @var \SimpleXMLElement */ $referenceOneMappingDoctrine = $referenceOneMapping; $referenceOneMapping = $referenceOneMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($referenceOneMapping->{'tree-parent'})) { $field = $this->_getAttribute($referenceOneMappingDoctrine, 'field'); if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) { - throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}"); + throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } $config['parent'] = $field; } + if (isset($referenceOneMapping->{'tree-root'})) { + $field = $this->_getAttribute($referenceOneMappingDoctrine, 'field'); + if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) { + throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->getName()}"); + } + $config['root'] = $field; + } } } - } elseif ($xmlDoctrine->getName() == 'entity') { + } elseif ('entity' === $xmlDoctrine->getName()) { if (isset($xmlDoctrine->{'many-to-one'})) { foreach ($xmlDoctrine->{'many-to-one'} as $manyToOneMapping) { /** - * @var \SimpleXMLElement $manyToOneMapping + * @var \SimpleXMLElement */ $manyToOneMappingDoctrine = $manyToOneMapping; $manyToOneMapping = $manyToOneMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($manyToOneMapping->{'tree-parent'})) { $field = $this->_getAttribute($manyToOneMappingDoctrine, 'field'); - $targetEntity = $meta->associationMappings[$field]['targetEntity']; + $targetEntity = $meta->getAssociationTargetClass($field); if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) { - throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}"); + throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } $config['parent'] = $field; } + if (isset($manyToOneMapping->{'tree-root'})) { + $field = $this->_getAttribute($manyToOneMappingDoctrine, 'field'); + $targetEntity = $meta->getAssociationTargetClass($field); + if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) { + throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->getName()}"); + } + $config['root'] = $field; + } } } - } elseif ($xmlDoctrine->getName() == 'document') { + } elseif ('document' === $xmlDoctrine->getName()) { if (isset($xmlDoctrine->{'reference-one'})) { foreach ($xmlDoctrine->{'reference-one'} as $referenceOneMapping) { /** - * @var \SimpleXMLElement $referenceOneMapping + * @var \SimpleXMLElement */ $referenceOneMappingDoctrine = $referenceOneMapping; $referenceOneMapping = $referenceOneMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($referenceOneMapping->{'tree-parent'})) { $field = $this->_getAttribute($referenceOneMappingDoctrine, 'field'); if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) { - throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}"); + throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } $config['parent'] = $field; } + if (isset($referenceOneMapping->{'tree-root'})) { + $field = $this->_getAttribute($referenceOneMappingDoctrine, 'field'); + if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) { + throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->getName()}"); + } + $config['root'] = $field; + } } } } if (!$meta->isMappedSuperclass && $config) { if (isset($config['strategy'])) { - if (is_array($meta->identifier) && count($meta->identifier) > 1) { - throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->name}"); + if (is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->getName()}"); } $method = 'validate'.ucfirst($config['strategy']).'TreeMetadata'; $validator->$method($meta, $config); } else { - throw new InvalidMappingException("Cannot find Tree type for class: {$meta->name}"); + throw new InvalidMappingException("Cannot find Tree type for class: {$meta->getName()}"); } } + + return $config; } } diff --git a/lib/Gedmo/Tree/Mapping/Driver/Yaml.php b/src/Tree/Mapping/Driver/Yaml.php similarity index 74% rename from lib/Gedmo/Tree/Mapping/Driver/Yaml.php rename to src/Tree/Mapping/Driver/Yaml.php index d06e85db31..d7cfe555cd 100644 --- a/lib/Gedmo/Tree/Mapping/Driver/Yaml.php +++ b/src/Tree/Mapping/Driver/Yaml.php @@ -1,10 +1,17 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Mapping\Driver; -use Gedmo\Mapping\Driver\File; -use Gedmo\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; use Gedmo\Tree\Mapping\Validator; /** @@ -14,12 +21,16 @@ * extension. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ class Yaml extends File implements Driver { /** * File extension + * * @var string */ protected $_extension = '.dcm.yml'; @@ -27,37 +38,33 @@ class Yaml extends File implements Driver /** * List of tree strategies available * - * @var array + * @var string[] */ - private $strategies = array( + private array $strategies = [ 'nested', 'closure', 'materializedPath', - ); + ]; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); $validator = new Validator(); if (isset($mapping['gedmo'])) { $classMapping = $mapping['gedmo']; if (isset($classMapping['tree']['type'])) { $strategy = $classMapping['tree']['type']; - if (!in_array($strategy, $this->strategies)) { + if (!in_array($strategy, $this->strategies, true)) { throw new InvalidMappingException("Tree type: $strategy is not available."); } $config['strategy'] = $strategy; - $config['activate_locking'] = isset($classMapping['tree']['activateLocking']) ? - $classMapping['tree']['activateLocking'] : false; + $config['activate_locking'] = $classMapping['tree']['activateLocking'] ?? false; $config['locking_timeout'] = isset($classMapping['tree']['lockingTimeout']) ? (int) $classMapping['tree']['lockingTimeout'] : 3; if ($config['locking_timeout'] < 1) { - throw new InvalidMappingException("Tree Locking Timeout must be at least of 1 second."); + throw new InvalidMappingException('Tree Locking Timeout must be at least of 1 second.'); } } if (isset($classMapping['tree']['closure'])) { @@ -69,13 +76,11 @@ public function readExtendedMetadata($meta, array &$config) } if (isset($mapping['id'])) { - foreach($mapping['id'] as $field => $fieldMapping) { + foreach ($mapping['id'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { - if (in_array('treePathSource', $fieldMapping['gedmo'])) { + if (in_array('treePathSource', $fieldMapping['gedmo'], true)) { if (!$validator->isValidFieldForPathSource($meta, $field)) { - throw new InvalidMappingException( - "Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->name}" - ); + throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->getName()}"); } $config['path_source'] = $field; } @@ -86,33 +91,36 @@ public function readExtendedMetadata($meta, array &$config) if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { - if (in_array('treeLeft', $fieldMapping['gedmo'])) { + if (in_array('treeLeft', $fieldMapping['gedmo'], true)) { if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } $config['left'] = $field; - } elseif (in_array('treeRight', $fieldMapping['gedmo'])) { + } elseif (in_array('treeRight', $fieldMapping['gedmo'], true)) { if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } $config['right'] = $field; - } elseif (in_array('treeLevel', $fieldMapping['gedmo'])) { + } elseif (in_array('treeLevel', $fieldMapping['gedmo'], true)) { if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}"); + throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); } $config['level'] = $field; - } elseif (in_array('treeRoot', $fieldMapping['gedmo'])) { + } elseif (in_array('treeRoot', $fieldMapping['gedmo'], true)) { if (!$validator->isValidFieldForRoot($meta, $field)) { - throw new InvalidMappingException("Tree root field - [{$field}] type is not valid and must be any of the 'integer' types or 'string' in class - {$meta->name}"); + throw new InvalidMappingException("Tree root field - [{$field}] type is not valid and must be any of the 'integer' types or 'string' in class - {$meta->getName()}"); } $config['root'] = $field; - } elseif (in_array('treePath', $fieldMapping['gedmo']) || isset($fieldMapping['gedmo']['treePath'])) { + } elseif (in_array('treePath', $fieldMapping['gedmo'], true) || isset($fieldMapping['gedmo']['treePath'])) { if (!$validator->isValidFieldForPath($meta, $field)) { - throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->name}"); + throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->getName()}"); } - $treePathInfo = isset($fieldMapping['gedmo']['treePath']) ? $fieldMapping['gedmo']['treePath'] : - $fieldMapping['gedmo'][array_search('treePath', $fieldMapping['gedmo'])]; + $treePathInfo = $fieldMapping['gedmo']['treePath'] ?? $fieldMapping['gedmo'][array_search( + 'treePath', + $fieldMapping['gedmo'], + true + )]; if (is_array($treePathInfo) && isset($treePathInfo['separator'])) { $separator = $treePathInfo['separator']; @@ -147,22 +155,22 @@ public function readExtendedMetadata($meta, array &$config) $config['path_append_id'] = $appendId; $config['path_starts_with_separator'] = $startsWithSeparator; $config['path_ends_with_separator'] = $endsWithSeparator; - } elseif (in_array('treePathSource', $fieldMapping['gedmo'])) { + } elseif (in_array('treePathSource', $fieldMapping['gedmo'], true)) { if (!$validator->isValidFieldForPathSource($meta, $field)) { - throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->name}"); + throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->getName()}"); } $config['path_source'] = $field; - } elseif (in_array('treePathHash', $fieldMapping['gedmo'])) { + } elseif (in_array('treePathHash', $fieldMapping['gedmo'], true)) { if (!$validator->isValidFieldForPathSource($meta, $field)) { - throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid and must be 'string' in class - {$meta->name}"); + throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid and must be 'string' in class - {$meta->getName()}"); } $config['path_hash'] = $field; - } elseif (in_array('treeLockTime', $fieldMapping['gedmo'])) { + } elseif (in_array('treeLockTime', $fieldMapping['gedmo'], true)) { if (!$validator->isValidFieldForLocktime($meta, $field)) { - throw new InvalidMappingException("Tree LockTime field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->name}"); + throw new InvalidMappingException("Tree LockTime field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->getName()}"); } $config['lock_time'] = $field; - } elseif (in_array('treeParent', $fieldMapping['gedmo'])) { + } elseif (in_array('treeParent', $fieldMapping['gedmo'], true)) { $config['parent'] = $field; } } @@ -170,38 +178,43 @@ public function readExtendedMetadata($meta, array &$config) } if (isset($config['activate_locking']) && $config['activate_locking'] && !isset($config['lock_time'])) { - throw new InvalidMappingException("You need to map a date|datetime|timestamp field as the tree lock time field to activate locking support."); + throw new InvalidMappingException('You need to map a date|datetime|timestamp field as the tree lock time field to activate locking support.'); } if (isset($mapping['manyToOne'])) { foreach ($mapping['manyToOne'] as $field => $relationMapping) { if (isset($relationMapping['gedmo'])) { - if (in_array('treeParent', $relationMapping['gedmo'])) { - if (!$rel = $this->getRelatedClassName($meta, $relationMapping['targetEntity'])) { - throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}"); + if (in_array('treeParent', $relationMapping['gedmo'], true)) { + if (!$this->getRelatedClassName($meta, $relationMapping['targetEntity'])) { + throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } $config['parent'] = $field; } + if (in_array('treeRoot', $relationMapping['gedmo'], true)) { + if (!$this->getRelatedClassName($meta, $relationMapping['targetEntity'])) { + throw new InvalidMappingException("Unable to find root-descendant relation through root field - [{$field}] in class - {$meta->getName()}"); + } + $config['root'] = $field; + } } } } if (!$meta->isMappedSuperclass && $config) { if (isset($config['strategy'])) { - if (is_array($meta->identifier) && count($meta->identifier) > 1) { - throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->name}"); + if (is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->getName()}"); } $method = 'validate'.ucfirst($config['strategy']).'TreeMetadata'; $validator->$method($meta, $config); } else { - throw new InvalidMappingException("Cannot find Tree type for class: {$meta->name}"); + throw new InvalidMappingException("Cannot find Tree type for class: {$meta->getName()}"); } } + + return $config; } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); diff --git a/lib/Gedmo/Tree/Mapping/Event/Adapter/ODM.php b/src/Tree/Mapping/Event/Adapter/ODM.php similarity index 56% rename from lib/Gedmo/Tree/Mapping/Event/Adapter/ODM.php rename to src/Tree/Mapping/Event/Adapter/ODM.php index 7ef605dfd8..f725babe9c 100644 --- a/lib/Gedmo/Tree/Mapping/Event/Adapter/ODM.php +++ b/src/Tree/Mapping/Event/Adapter/ODM.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Mapping\Event\Adapter; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; @@ -10,7 +17,6 @@ * for Tree behavior * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ODM extends BaseAdapterODM implements TreeAdapter { diff --git a/lib/Gedmo/Tree/Mapping/Event/Adapter/ORM.php b/src/Tree/Mapping/Event/Adapter/ORM.php similarity index 56% rename from lib/Gedmo/Tree/Mapping/Event/Adapter/ORM.php rename to src/Tree/Mapping/Event/Adapter/ORM.php index c0ebd635f9..d4157474ff 100644 --- a/lib/Gedmo/Tree/Mapping/Event/Adapter/ORM.php +++ b/src/Tree/Mapping/Event/Adapter/ORM.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Mapping\Event\Adapter; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; @@ -10,7 +17,6 @@ * for Tree behavior * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ final class ORM extends BaseAdapterORM implements TreeAdapter { diff --git a/src/Tree/Mapping/Event/TreeAdapter.php b/src/Tree/Mapping/Event/TreeAdapter.php new file mode 100644 index 0000000000..239e2c8035 --- /dev/null +++ b/src/Tree/Mapping/Event/TreeAdapter.php @@ -0,0 +1,21 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree\Mapping\Event; + +use Gedmo\Mapping\Event\AdapterInterface; + +/** + * Doctrine event adapter for the Tree extension. + * + * @author Gediminas Morkevicius + */ +interface TreeAdapter extends AdapterInterface +{ +} diff --git a/lib/Gedmo/Tree/Mapping/Validator.php b/src/Tree/Mapping/Validator.php similarity index 55% rename from lib/Gedmo/Tree/Mapping/Validator.php rename to src/Tree/Mapping/Validator.php index 23db3bae61..835b658ddc 100644 --- a/lib/Gedmo/Tree/Mapping/Validator.php +++ b/src/Tree/Mapping/Validator.php @@ -1,7 +1,16 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Mapping; +use Doctrine\ORM\Mapping\FieldMapping; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; /** @@ -12,38 +21,39 @@ * @author Gustavo Falco * @author Gediminas Morkevicius * @author - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ class Validator { /** * List of types which are valid for tree fields * - * @var array + * @var string[] */ - private $validTypes = array( + private const VALID_TYPES = [ 'integer', 'smallint', 'bigint', 'int', - ); + ]; /** * List of types which are valid for the path (materialized path strategy) * - * @var array + * @var string[] */ - private $validPathTypes = array( + private array $validPathTypes = [ 'string', 'text', - ); + ]; /** * List of types which are valid for the path source (materialized path strategy) * - * @var array + * @var string[] */ - private $validPathSourceTypes = array( + private array $validPathSourceTypes = [ 'id', 'integer', 'smallint', @@ -51,132 +61,135 @@ class Validator 'string', 'int', 'float', - ); + 'uuid', + ]; /** * List of types which are valid for the path hash (materialized path strategy) * - * @var array + * @var string[] */ - private $validPathHashTypes = array( + private array $validPathHashTypes = [ 'string', - ); + ]; /** * List of types which are valid for the path source (materialized path strategy) * - * @var array + * @var string[] */ - private $validRootTypes = array( + private array $validRootTypes = [ 'integer', 'smallint', 'bigint', 'int', 'string', 'guid', - ); + ]; /** * Checks if $field type is valid * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ public function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validTypes); + return $mapping && in_array($this->getMappingType($mapping), self::VALID_TYPES, true); } /** * Checks if $field type is valid for Path field * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ public function isValidFieldForPath($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validPathTypes); + return $mapping && in_array($this->getMappingType($mapping), $this->validPathTypes, true); } /** * Checks if $field type is valid for PathSource field * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ public function isValidFieldForPathSource($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validPathSourceTypes); + return $mapping && in_array($this->getMappingType($mapping), $this->validPathSourceTypes, true); } /** * Checks if $field type is valid for PathHash field * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ public function isValidFieldForPathHash($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validPathHashTypes); + return $mapping && in_array($this->getMappingType($mapping), $this->validPathHashTypes, true); } /** * Checks if $field type is valid for LockTime field * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ public function isValidFieldForLockTime($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && ($mapping['type'] === 'date' || $mapping['type'] === 'datetime' || $mapping['type'] === 'timestamp'); + return $mapping && ('date' === $this->getMappingType($mapping) || 'datetime' === $this->getMappingType($mapping) || 'timestamp' === $this->getMappingType($mapping)); } /** * Checks if $field type is valid for Root field * - * @param object $meta - * @param string $field + * @param ClassMetadata $meta + * @param string $field * - * @return boolean + * @return bool */ public function isValidFieldForRoot($meta, $field) { $mapping = $meta->getFieldMapping($field); - return $mapping && in_array($mapping['type'], $this->validRootTypes); + return $mapping && in_array($this->getMappingType($mapping), $this->validRootTypes, true); } /** * Validates metadata for nested type tree * - * @param object $meta - * @param array $config + * @param ClassMetadata $meta + * @param array $config * * @throws InvalidMappingException + * + * @return void */ public function validateNestedTreeMetadata($meta, array $config) { - $missingFields = array(); + $missingFields = []; if (!isset($config['parent'])) { $missingFields[] = 'ancestor'; } @@ -187,21 +200,23 @@ public function validateNestedTreeMetadata($meta, array $config) $missingFields[] = 'right'; } if ($missingFields) { - throw new InvalidMappingException("Missing properties: ".implode(', ', $missingFields)." in class - {$meta->name}"); + throw new InvalidMappingException('Missing properties: '.implode(', ', $missingFields)." in class - {$meta->getName()}"); } } /** * Validates metadata for closure type tree * - * @param object $meta - * @param array $config + * @param ClassMetadata $meta + * @param array $config * * @throws InvalidMappingException + * + * @return void */ public function validateClosureTreeMetadata($meta, array $config) { - $missingFields = array(); + $missingFields = []; if (!isset($config['parent'])) { $missingFields[] = 'ancestor'; } @@ -209,21 +224,23 @@ public function validateClosureTreeMetadata($meta, array $config) $missingFields[] = 'closure class'; } if ($missingFields) { - throw new InvalidMappingException("Missing properties: ".implode(', ', $missingFields)." in class - {$meta->name}"); + throw new InvalidMappingException('Missing properties: '.implode(', ', $missingFields)." in class - {$meta->getName()}"); } } /** * Validates metadata for materialized path type tree * - * @param object $meta - * @param array $config + * @param ClassMetadata $meta + * @param array $config * * @throws InvalidMappingException + * + * @return void */ public function validateMaterializedPathTreeMetadata($meta, array $config) { - $missingFields = array(); + $missingFields = []; if (!isset($config['parent'])) { $missingFields[] = 'ancestor'; } @@ -234,7 +251,19 @@ public function validateMaterializedPathTreeMetadata($meta, array $config) $missingFields[] = 'path_source'; } if ($missingFields) { - throw new InvalidMappingException("Missing properties: ".implode(', ', $missingFields)." in class - {$meta->name}"); + throw new InvalidMappingException('Missing properties: '.implode(', ', $missingFields)." in class - {$meta->getName()}"); } } + + /** + * @param FieldMapping|array $mapping + */ + private function getMappingType($mapping): string + { + if ($mapping instanceof FieldMapping) { + return $mapping->type; + } + + return $mapping['type']; + } } diff --git a/src/Tree/Node.php b/src/Tree/Node.php new file mode 100644 index 0000000000..d10a61c648 --- /dev/null +++ b/src/Tree/Node.php @@ -0,0 +1,52 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree; + +/** + * Marker interface for objects which can be identified as a tree node. + * + * @method void setSibling(self $node) + * @method ?self getSibling() + * + * @author Gediminas Morkevicius + */ +interface Node +{ + // use now annotations instead of predefined methods, this interface is not necessary + + /* + * @Gedmo\TreeLeft + * to mark the field as "tree left" use property annotation @Gedmo\TreeLeft + * it will use this field to store tree left value + */ + + /* + * @Gedmo\TreeRight + * to mark the field as "tree right" use property annotation @Gedmo\TreeRight + * it will use this field to store tree right value + */ + + /* + * @Gedmo\TreeParent + * in every tree there should be link to parent. To identify a relation + * as parent relation to child use @Tree:Ancestor annotation on the related property + */ + + /* + * @Gedmo\TreeLevel + * level of node. + */ + + // @todo: In the next major release, remove this line and uncomment the method in the next line. + // public function setSibling(self $node): void; + + // @todo: In the next major release, remove this line and uncomment the method in the next line. + // public function getSibling(): ?self; +} diff --git a/src/Tree/RepositoryInterface.php b/src/Tree/RepositoryInterface.php new file mode 100644 index 0000000000..1768d0a810 --- /dev/null +++ b/src/Tree/RepositoryInterface.php @@ -0,0 +1,81 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree; + +use Gedmo\Exception\InvalidArgumentException; + +/** + * This interface ensures a consistent API between repositories for the ORM and the ODM. + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @template T of object + */ +interface RepositoryInterface extends RepositoryUtilsInterface +{ + /** + * Get all root nodes. + * + * @param string $sortByField + * @param string $direction + * + * @return iterable + * + * @phpstan-return iterable + */ + public function getRootNodes($sortByField = null, $direction = 'asc'); + + /** + * Returns an array of nodes optimized for building a tree. + * + * @param object $node Root node + * @param bool $direct Flag indicating whether only direct children should be retrieved + * @param array $options Options, see {@see RepositoryUtilsInterface::buildTree()} for supported keys + * @param bool $includeNode Flag indicating whether the given node should be included in the results + * + * @phpstan-param T $node + * + * @return array + * + * @phpstan-return iterable + */ + public function getNodesHierarchy($node = null, $direct = false, array $options = [], $includeNode = false); + + /** + * Get the list of children for the given node. + * + * @param object|null $node If null, all tree nodes will be taken + * @param bool $direct True to take only direct children + * @param string|string[]|null $sortByField Field name or array of fields names to sort by + * @param string|string[] $direction Sort order ('asc'|'desc'|'ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements + * @param bool $includeNode Include the root node in results? + * + * @phpstan-param 'asc'|'desc'|'ASC'|'DESC'|array $direction + * @phpstan-param T|null $node + * + * @return iterable List of children + * + * @phpstan-return iterable + */ + public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); + + /** + * Counts the children of the given node + * + * @param object|null $node The object to count children for; if null, all nodes will be counted + * @param bool $direct Flag indicating whether only direct children should be counted + * + * @throws InvalidArgumentException if the input is invalid + * + * @return int + */ + public function childCount($node = null, $direct = false); +} diff --git a/lib/Gedmo/Tree/RepositoryUtils.php b/src/Tree/RepositoryUtils.php similarity index 66% rename from lib/Gedmo/Tree/RepositoryUtils.php rename to src/Tree/RepositoryUtils.php index bb5cade06d..cad3eafe91 100644 --- a/lib/Gedmo/Tree/RepositoryUtils.php +++ b/src/Tree/RepositoryUtils.php @@ -1,23 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; use Gedmo\Exception\InvalidArgumentException; - +use Gedmo\Tool\Wrapper\EntityWrapper; +use Gedmo\Tool\Wrapper\MongoDocumentWrapper; + +/** + * @final since gedmo/doctrine-extensions 3.11 + * + * @template T of object + */ class RepositoryUtils implements RepositoryUtilsInterface { - /** @var \Doctrine\Common\Persistence\Mapping\ClassMetadata */ + /** @var ClassMetadata */ protected $meta; - /** @var \Gedmo\Tree\TreeListener */ + /** @var TreeListener */ protected $listener; - /** @var \Doctrine\Common\Persistence\ObjectManager */ + /** @var ObjectManager&(DocumentManager|EntityManagerInterface) */ protected $om; - /** @var \Gedmo\Tree\RepositoryInterface */ + /** @var RepositoryInterface */ protected $repo; /** @@ -28,6 +44,12 @@ class RepositoryUtils implements RepositoryUtilsInterface */ protected $childrenIndex = '__children'; + /** + * @param ObjectManager&(DocumentManager|EntityManagerInterface) $om + * @param ClassMetadata $meta + * @param TreeListener $listener + * @param RepositoryInterface $repo + */ public function __construct(ObjectManager $om, ClassMetadata $meta, $listener, $repo) { $this->om = $om; @@ -36,26 +58,26 @@ public function __construct(ObjectManager $om, ClassMetadata $meta, $listener, $ $this->repo = $repo; } + /** + * @return ClassMetadata + */ public function getClassMetadata() { return $this->meta; } - /** - * {@inheritDoc} - */ - public function childrenHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false) + public function childrenHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { $meta = $this->getClassMetadata(); - if ($node !== null) { - if ($node instanceof $meta->name) { - $wrapperClass = $this->om instanceof \Doctrine\ORM\EntityManager ? - '\Gedmo\Tool\Wrapper\EntityWrapper' : - '\Gedmo\Tool\Wrapper\MongoDocumentWrapper'; + if (null !== $node) { + if (is_a($node, $meta->getName())) { + $wrapperClass = $this->om instanceof EntityManagerInterface ? + EntityWrapper::class : + MongoDocumentWrapper::class; $wrapped = new $wrapperClass($node, $this->om); if (!$wrapped->hasValidIdentifier()) { - throw new InvalidArgumentException("Node is not managed by UnitOfWork"); + throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } } } else { @@ -68,51 +90,48 @@ public function childrenHierarchy($node = null, $direct = false, array $options return $this->repo->buildTree($nodes, $options); } - /** - * {@inheritDoc} - */ - public function buildTree(array $nodes, array $options = array()) + public function buildTree(array $nodes, array $options = []) { $meta = $this->getClassMetadata(); $nestedTree = $this->repo->buildTreeArray($nodes); - $default = array( + $default = [ 'decorate' => false, 'rootOpen' => '
      ', 'rootClose' => '
    ', 'childOpen' => '
  • ', 'childClose' => '
  • ', - 'nodeDecorator' => function ($node) use ($meta) { + 'nodeDecorator' => static function ($node) use ($meta) { // override and change it, guessing which field to use if ($meta->hasField('title')) { $field = 'title'; } elseif ($meta->hasField('name')) { $field = 'name'; } else { - throw new InvalidArgumentException("Cannot find any representation field"); + throw new InvalidArgumentException('Cannot find any representation field'); } return $node[$field]; }, - ); + ]; $options = array_merge($default, $options); // If you don't want any html output it will return the nested array if (!$options['decorate']) { return $nestedTree; } - if (!count($nestedTree)) { + if ([] === $nestedTree) { return ''; } $childrenIndex = $this->childrenIndex; - $build = function ($tree) use (&$build, &$options, $childrenIndex) { + $build = static function ($tree) use (&$build, &$options, $childrenIndex) { $output = is_string($options['rootOpen']) ? $options['rootOpen'] : $options['rootOpen']($tree); foreach ($tree as $node) { $output .= is_string($options['childOpen']) ? $options['childOpen'] : $options['childOpen']($node); $output .= $options['nodeDecorator']($node); - if (count($node[$childrenIndex]) > 0) { + if ([] !== $node[$childrenIndex]) { $output .= $build($node[$childrenIndex]); } $output .= is_string($options['childClose']) ? $options['childClose'] : $options['childClose']($node); @@ -124,31 +143,28 @@ public function buildTree(array $nodes, array $options = array()) return $build($nestedTree); } - /** - * {@inheritDoc} - */ public function buildTreeArray(array $nodes) { $meta = $this->getClassMetadata(); - $config = $this->listener->getConfiguration($this->om, $meta->name); - $nestedTree = array(); + $config = $this->listener->getConfiguration($this->om, $meta->getName()); + $nestedTree = []; $l = 0; - if (count($nodes) > 0) { + if ([] !== $nodes) { // Node Stack. Used to help building the hierarchy - $stack = array(); + $stack = []; foreach ($nodes as $child) { $item = $child; - $item[$this->childrenIndex] = array(); + $item[$this->childrenIndex] = []; // Number of stack items $l = count($stack); // Check if we're dealing with different levels while ($l > 0 && $stack[$l - 1][$config['level']] >= $item[$config['level']]) { array_pop($stack); - $l--; + --$l; } // Stack is empty (we are inspecting the root) - if ($l == 0) { + if (0 == $l) { // Assigning the root child $i = count($nestedTree); $nestedTree[$i] = $item; @@ -165,17 +181,11 @@ public function buildTreeArray(array $nodes) return $nestedTree; } - /** - * {@inheritDoc} - */ public function setChildrenIndex($childrenIndex) { $this->childrenIndex = $childrenIndex; } - /** - * {@inheritDoc} - */ public function getChildrenIndex() { return $this->childrenIndex; diff --git a/src/Tree/RepositoryUtilsInterface.php b/src/Tree/RepositoryUtilsInterface.php new file mode 100644 index 0000000000..a564d283bc --- /dev/null +++ b/src/Tree/RepositoryUtilsInterface.php @@ -0,0 +1,85 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree; + +use Gedmo\Exception\InvalidArgumentException; + +interface RepositoryUtilsInterface +{ + /** + * Retrieves the nested array or decorated output. + * + * Uses options to handle decorations + * + * @param object|null $node The object to fetch children for; if null, all nodes will be retrieved + * @param bool $direct Flag indicating whether only direct children should be retrieved + * @param array $options Options configuring the output, supported keys include: + * - decorate: boolean (false) - retrieves the tree as an HTML `
      ` element + * - nodeDecorator: Closure (null) - uses $node as argument and returns the decorated item as a string + * - rootOpen: string || Closure ('
        ') - branch start, Closure will be given $children as a parameter + * - rootClose: string ('
      ') - branch close + * - childOpen: string || Closure ('
    • ') - start of node, Closure will be given $node as a parameter + * - childClose: string ('
    • ') - close of node + * - childSort: array || keys allowed: field: field to sort on, dir: direction. 'asc' or 'desc' + * @param bool $includeNode Flag indicating whether the given node should be included in the results + * + * @throws InvalidArgumentException + * + * @return array>|string + */ + public function childrenHierarchy($node = null, $direct = false, array $options = [], $includeNode = false); + + /** + * Retrieves the nested array or the decorated output. + * + * Uses options to handle decorations + * + * NOTE: nodes should be fetched and hydrated as array + * + * @param array $nodes The nodes to build the tree from + * @param array $options Options configuring the output, supported keys include: + * - decorate: boolean (false) - retrieves the tree as an HTML `
        ` element + * - nodeDecorator: Closure (null) - uses $node as argument and returns the decorated item as a string + * - rootOpen: string || Closure ('
          ') - branch start, Closure will be given $children as a parameter + * - rootClose: string ('
        ') - branch close + * - childOpen: string || Closure ('
      • ') - start of node, Closure will be given $node as a parameter + * - childClose: string ('
      • ') - close of node + * + * @throws InvalidArgumentException + * + * @return array>|string + */ + public function buildTree(array $nodes, array $options = []); + + /** + * Process a list of nodes and produce an array with the structure of the tree. + * + * @param object[] $nodes The nodes to build the tree from + * + * @return array> + */ + public function buildTreeArray(array $nodes); + + /** + * Sets the current children index. + * + * @param string $childrenIndex + * + * @return void + */ + public function setChildrenIndex($childrenIndex); + + /** + * Gets the current children index. + * + * @return string + */ + public function getChildrenIndex(); +} diff --git a/lib/Gedmo/Tree/Strategy.php b/src/Tree/Strategy.php similarity index 57% rename from lib/Gedmo/Tree/Strategy.php rename to src/Tree/Strategy.php index bbaf206a36..b06f5ebc4a 100644 --- a/lib/Gedmo/Tree/Strategy.php +++ b/src/Tree/Strategy.php @@ -1,8 +1,16 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\Event\AdapterInterface; interface Strategy @@ -10,46 +18,45 @@ interface Strategy /** * NestedSet strategy */ - const NESTED = 'nested'; + public const NESTED = 'nested'; /** * Closure strategy */ - const CLOSURE = 'closure'; + public const CLOSURE = 'closure'; /** * Materialized Path strategy */ - const MATERIALIZED_PATH = 'materializedPath'; + public const MATERIALIZED_PATH = 'materializedPath'; /** - * Get the name of strategy - * - * @return string + * Create a new strategy instance */ - public function getName(); + public function __construct(TreeListener $listener); /** - * Initialize strategy with tree listener + * Get the name of the strategy * - * @param TreeListener $listener + * @return string */ - public function __construct(TreeListener $listener); + public function getName(); /** * Operations after metadata is loaded * - * @param ObjectManager $om - * @param object $meta + * @param ObjectManager $om + * @param ClassMetadata $meta + * + * @return void */ public function processMetadataLoad($om, $meta); /** * Operations on tree node insertion * - * @param ObjectManager $om - object manager - * @param object $object - node - * @param AdapterInterface $ea - event adapter + * @param ObjectManager $om + * @param object|Node $object * * @return void */ @@ -58,9 +65,8 @@ public function processScheduledInsertion($om, $object, AdapterInterface $ea); /** * Operations on tree node updates * - * @param ObjectManager $om - object manager - * @param object $object - node - * @param AdapterInterface $ea - event adapter + * @param ObjectManager $om + * @param object|Node $object * * @return void */ @@ -69,8 +75,8 @@ public function processScheduledUpdate($om, $object, AdapterInterface $ea); /** * Operations on tree node delete * - * @param ObjectManager $om - object manager - * @param object $object - node + * @param ObjectManager $om + * @param object|Node $object * * @return void */ @@ -79,8 +85,8 @@ public function processScheduledDelete($om, $object); /** * Operations on tree node removal * - * @param ObjectManager $om - object manager - * @param object $object - node + * @param ObjectManager $om + * @param object|Node $object * * @return void */ @@ -89,8 +95,8 @@ public function processPreRemove($om, $object); /** * Operations on tree node persist * - * @param ObjectManager $om - object manager - * @param object $object - node + * @param ObjectManager $om + * @param object|Node $object * * @return void */ @@ -99,8 +105,8 @@ public function processPrePersist($om, $object); /** * Operations on tree node update * - * @param ObjectManager $om - object manager - * @param object $object - node + * @param ObjectManager $om + * @param object|Node $object * * @return void */ @@ -109,9 +115,8 @@ public function processPreUpdate($om, $object); /** * Operations on tree node insertions * - * @param ObjectManager $om - object manager - * @param object $object - node - * @param AdapterInterface $ea - event adapter + * @param ObjectManager $om + * @param object|Node $object * * @return void */ @@ -120,9 +125,8 @@ public function processPostPersist($om, $object, AdapterInterface $ea); /** * Operations on tree node updates * - * @param ObjectManager $om - object manager - * @param object $object - node - * @param AdapterInterface $ea - event adapter + * @param ObjectManager $om + * @param object|Node $object * * @return void */ @@ -131,9 +135,8 @@ public function processPostUpdate($om, $object, AdapterInterface $ea); /** * Operations on tree node removals * - * @param ObjectManager $om - object manager - * @param object $object - node - * @param AdapterInterface $ea - event adapter + * @param ObjectManager $om + * @param object|Node $object * * @return void */ @@ -142,8 +145,7 @@ public function processPostRemove($om, $object, AdapterInterface $ea); /** * Operations on the end of flush process * - * @param ObjectManager $om - object manager - * @param AdapterInterface $ea - event adapter + * @param ObjectManager $om * * @return void */ diff --git a/lib/Gedmo/Tree/Strategy/AbstractMaterializedPath.php b/src/Tree/Strategy/AbstractMaterializedPath.php similarity index 64% rename from lib/Gedmo/Tree/Strategy/AbstractMaterializedPath.php rename to src/Tree/Strategy/AbstractMaterializedPath.php index e33e55fcc0..8a903b26e2 100644 --- a/lib/Gedmo/Tree/Strategy/AbstractMaterializedPath.php +++ b/src/Tree/Strategy/AbstractMaterializedPath.php @@ -1,14 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Strategy; -use Gedmo\Tree\Strategy; -use Gedmo\Tree\TreeListener; -use Doctrine\Common\Persistence\ObjectManager; use Doctrine\ODM\MongoDB\UnitOfWork as MongoDBUnitOfWork; -use Gedmo\Mapping\Event\AdapterInterface; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; use Gedmo\Exception\RuntimeException; use Gedmo\Exception\TreeLockingException; +use Gedmo\Mapping\Event\AdapterInterface; +use Gedmo\Tree\Node; +use Gedmo\Tree\Strategy; +use Gedmo\Tree\TreeListener; +use MongoDB\BSON\UTCDateTime; +use ProxyManager\Proxy\GhostObjectInterface; /** * This strategy makes tree using materialized path strategy @@ -16,105 +27,90 @@ * @author Gustavo Falco * @author Gediminas Morkevicius * @author - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ - abstract class AbstractMaterializedPath implements Strategy { - const ACTION_INSERT = 'insert'; - const ACTION_UPDATE = 'update'; - const ACTION_REMOVE = 'remove'; + public const ACTION_INSERT = 'insert'; + public const ACTION_UPDATE = 'update'; + public const ACTION_REMOVE = 'remove'; /** - * TreeListener - * - * @var AbstractTreeListener + * @var TreeListener */ - protected $listener = null; + protected $listener; /** * Array of objects which were scheduled for path processes * - * @var array + * @var array */ - protected $scheduledForPathProcess = array(); + protected $scheduledForPathProcess = []; /** * Array of objects which were scheduled for path process. * This time, this array contains the objects with their ID * already set * - * @var array + * @var array */ - protected $scheduledForPathProcessWithIdSet = array(); + protected $scheduledForPathProcessWithIdSet = []; /** * Roots of trees which needs to be locked * - * @var array + * @var array */ - protected $rootsOfTreesWhichNeedsLocking = array(); + protected $rootsOfTreesWhichNeedsLocking = []; /** * Objects which are going to be inserted (set only if tree locking is used) * - * @var array + * @var array */ - protected $pendingObjectsToInsert = array(); + protected $pendingObjectsToInsert = []; /** * Objects which are going to be updated (set only if tree locking is used) * - * @var array + * @var array */ - protected $pendingObjectsToUpdate = array(); + protected $pendingObjectsToUpdate = []; /** * Objects which are going to be removed (set only if tree locking is used) * - * @var array + * @var array */ - protected $pendingObjectsToRemove = array(); + protected $pendingObjectsToRemove = []; - /** - * {@inheritdoc} - */ public function __construct(TreeListener $listener) { $this->listener = $listener; } - /** - * {@inheritdoc} - */ public function getName() { return Strategy::MATERIALIZED_PATH; } - /** - * {@inheritdoc} - */ public function processScheduledInsertion($om, $node, AdapterInterface $ea) { $meta = $om->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($om, $meta->name); - $fieldMapping = $meta->getFieldMapping($config['path_source']); - if ($meta->isIdentifier($config['path_source']) || $fieldMapping['type'] === 'string') { - $this->scheduledForPathProcess[spl_object_hash($node)] = $node; + // ID is always used in a path, + // and if it is generated value from engine (like AUTO_INCREMENT), + // we need to schedule the path update + if ([] === $meta->getIdentifierValues($node)) { + $this->scheduledForPathProcess[spl_object_id($node)] = $node; } else { $this->updateNode($om, $node, $ea); } } - /** - * {@inheritdoc} - */ public function processScheduledUpdate($om, $node, AdapterInterface $ea) { $meta = $om->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($om, $meta->name); + $config = $this->listener->getConfiguration($om, $meta->getName()); $uow = $om->getUnitOfWork(); $changeSet = $ea->getObjectChangeSet($uow, $node); @@ -122,9 +118,7 @@ public function processScheduledUpdate($om, $node, AdapterInterface $ea) if (isset($changeSet[$config['path']])) { $originalPath = $changeSet[$config['path']][0]; } else { - $pathProp = $meta->getReflectionProperty($config['path']); - $pathProp->setAccessible(true); - $originalPath = $pathProp->getValue($node); + $originalPath = $meta->getFieldValue($node, $config['path']); } $this->updateNode($om, $node, $ea); @@ -132,12 +126,9 @@ public function processScheduledUpdate($om, $node, AdapterInterface $ea) } } - /** - * {@inheritdoc} - */ public function processPostPersist($om, $node, AdapterInterface $ea) { - $oid = spl_object_hash($node); + $oid = spl_object_id($node); if ($this->scheduledForPathProcess && array_key_exists($oid, $this->scheduledForPathProcess)) { $this->scheduledForPathProcessWithIdSet[$oid] = $node; @@ -156,68 +147,44 @@ public function processPostPersist($om, $node, AdapterInterface $ea) $this->processPostEventsActions($om, $ea, $node, self::ACTION_INSERT); } - /** - * {@inheritdoc} - */ public function processPostUpdate($om, $node, AdapterInterface $ea) { $this->processPostEventsActions($om, $ea, $node, self::ACTION_UPDATE); } - /** - * {@inheritdoc} - */ public function processPostRemove($om, $node, AdapterInterface $ea) { $this->processPostEventsActions($om, $ea, $node, self::ACTION_REMOVE); } - /** - * {@inheritdoc} - */ public function onFlushEnd($om, AdapterInterface $ea) { $this->lockTrees($om, $ea); } - /** - * {@inheritdoc} - */ public function processPreRemove($om, $node) { $this->processPreLockingActions($om, $node, self::ACTION_REMOVE); } - /** - * {@inheritdoc} - */ public function processPrePersist($om, $node) { $this->processPreLockingActions($om, $node, self::ACTION_INSERT); } - /** - * {@inheritdoc} - */ public function processPreUpdate($om, $node) { $this->processPreLockingActions($om, $node, self::ACTION_UPDATE); } - /** - * {@inheritdoc} - */ public function processMetadataLoad($om, $meta) { } - /** - * {@inheritdoc} - */ public function processScheduledDelete($om, $node) { $meta = $om->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($om, $meta->name); + $config = $this->listener->getConfiguration($om, $meta->getName()); $this->removeNode($om, $meta, $config, $node); } @@ -225,29 +192,21 @@ public function processScheduledDelete($om, $node) /** * Update the $node * - * @param ObjectManager $om - * @param object $node - target node - * @param AdapterInterface $ea - event adapter + * @param object $node target node + * @param AdapterInterface $ea event adapter * * @return void */ public function updateNode(ObjectManager $om, $node, AdapterInterface $ea) { - $oid = spl_object_hash($node); $meta = $om->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($om, $meta->name); + $config = $this->listener->getConfiguration($om, $meta->getName()); $uow = $om->getUnitOfWork(); - $parentProp = $meta->getReflectionProperty($config['parent']); - $parentProp->setAccessible(true); - $parent = $parentProp->getValue($node); - $pathProp = $meta->getReflectionProperty($config['path']); - $pathProp->setAccessible(true); - $pathSourceProp = $meta->getReflectionProperty($config['path_source']); - $pathSourceProp->setAccessible(true); - $path = $pathSourceProp->getValue($node); + $parent = $meta->getFieldValue($node, $config['parent']); + $path = (string) $meta->getFieldValue($node, $config['path_source']); // We need to avoid the presence of the path separator in the path source - if (strpos($path, $config['path_separator']) !== false) { + if (false !== strpos($path, $config['path_separator'])) { $msg = 'You can\'t use the Path separator ("%s") as a character for your PathSource field value.'; throw new RuntimeException(sprintf($msg, $config['path_separator'])); @@ -258,13 +217,11 @@ public function updateNode(ObjectManager $om, $node, AdapterInterface $ea) // default behavior: if PathSource field is a string, we append the ID to the path // path_append_id is true: always append id // path_append_id is false: never append id - if ($config['path_append_id'] === true || ($fieldMapping['type'] === 'string' && $config['path_append_id'] !== false)) { + if (true === $config['path_append_id'] || ('string' === ($fieldMapping->type ?? $fieldMapping['type']) && false !== $config['path_append_id'])) { if (method_exists($meta, 'getIdentifierValue')) { $identifier = $meta->getIdentifierValue($node); } else { - $identifierProp = $meta->getReflectionProperty($meta->getSingleIdentifierFieldName()); - $identifierProp->setAccessible(true); - $identifier = $identifierProp->getValue($node); + $identifier = $meta->getFieldValue($node, $meta->getSingleIdentifierFieldName()); } $path .= '-'.$identifier; @@ -277,18 +234,18 @@ public function updateNode(ObjectManager $om, $node, AdapterInterface $ea) $changeSet = $uow->isScheduledForUpdate($parent) ? $ea->getObjectChangeSet($uow, $parent) : false; $pathOrPathSourceHasChanged = $changeSet && (isset($changeSet[$config['path_source']]) || isset($changeSet[$config['path']])); - if ($pathOrPathSourceHasChanged || !$pathProp->getValue($parent)) { + if ($pathOrPathSourceHasChanged || !$meta->getFieldValue($node, $config['path'])) { $this->updateNode($om, $parent, $ea); } - $parentPath = $pathProp->getValue($parent); + $parentPath = $meta->getFieldValue($parent, $config['path']); // if parent path not ends with separator if ($parentPath[strlen($parentPath) - 1] !== $config['path_separator']) { // add separator - $path = $pathProp->getValue($parent).$config['path_separator'].$path; + $path = $parentPath.$config['path_separator'].$path; } else { // don't add separator - $path = $pathProp->getValue($parent).$path; + $path = $parentPath.$path; } } @@ -300,52 +257,67 @@ public function updateNode(ObjectManager $om, $node, AdapterInterface $ea) $path .= $config['path_separator']; } - $pathProp->setValue($node, $path); - $changes = array( - $config['path'] => array(null, $path), - ); + $meta->setFieldValue($node, $config['path'], $path); + $changes = [ + $config['path'] => [null, $path], + ]; + + $pathHash = null; if (isset($config['path_hash'])) { $pathHash = md5($path); - $pathHashProp = $meta->getReflectionProperty($config['path_hash']); - $pathHashProp->setAccessible(true); - $pathHashProp->setValue($node, $pathHash); - $changes[$config['path_hash']] = array(null, $pathHash); + $meta->setFieldValue($node, $config['path_hash'], $pathHash); + $changes[$config['path_hash']] = [null, $pathHash]; + } + + if (isset($config['root'])) { + $root = null; + + // Define the root value by grabbing the top of the current path + $rootFinderPath = explode($config['path_separator'], $path); + $rootIndex = $config['path_starts_with_separator'] ? 1 : 0; + $root = $rootFinderPath[$rootIndex]; + + // If it is an association, then make it an reference + // to the entity + if ($meta->hasAssociation($config['root'])) { + $rootClass = $meta->getAssociationTargetClass($config['root']); + $root = $om->getReference($rootClass, $root); + } + + $meta->setFieldValue($node, $config['root'], $root); + $changes[$config['root']] = [null, $root]; } if (isset($config['level'])) { $level = substr_count($path, $config['path_separator']); - $levelProp = $meta->getReflectionProperty($config['level']); - $levelProp->setAccessible(true); - $levelProp->setValue($node, $level); - $changes[$config['level']] = array(null, $level); + $meta->setFieldValue($node, $config['level'], $level); + $changes[$config['level']] = [null, $level]; } if (!$uow instanceof MongoDBUnitOfWork) { - $ea->setOriginalObjectProperty($uow, $oid, $config['path'], $path); + $ea->setOriginalObjectProperty($uow, $node, $config['path'], $path); $uow->scheduleExtraUpdate($node, $changes); } else { $ea->recomputeSingleObjectChangeSet($uow, $meta, $node); } if (isset($config['path_hash'])) { - $ea->setOriginalObjectProperty($uow, $oid, $config['path_hash'], $pathHash); + $ea->setOriginalObjectProperty($uow, $node, $config['path_hash'], $pathHash); } } /** * Update node's children * - * @param ObjectManager $om - * @param object $node - * @param AdapterInterface $ea - * @param string $originalPath + * @param object $node + * @param string $originalPath * * @return void */ public function updateChildren(ObjectManager $om, $node, AdapterInterface $ea, $originalPath) { $meta = $om->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($om, $meta->name); + $config = $this->listener->getConfiguration($om, $meta->getName()); $children = $this->getChildren($om, $meta, $config, $originalPath); foreach ($children as $child) { @@ -365,47 +337,39 @@ public function updateChildren(ObjectManager $om, $node, AdapterInterface $ea, $ public function processPreLockingActions($om, $node, $action) { $meta = $om->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($om, $meta->name); + $config = $this->listener->getConfiguration($om, $meta->getName()); if ($config['activate_locking']) { - ; - $parentProp = $meta->getReflectionProperty($config['parent']); - $parentProp->setAccessible(true); $parentNode = $node; - while (!is_null($parent = $parentProp->getValue($parentNode))) { + while (($parent = $meta->getFieldValue($parentNode, $config['parent'])) !== null) { $parentNode = $parent; } // In some cases, the parent could be a not initialized proxy. In this case, the // "lockTime" field may NOT be loaded yet and have null instead of the date. // We need to be sure that this field has its real value - if ($parentNode !== $node && $parentNode instanceof \Doctrine\ODM\MongoDB\Proxy\Proxy) { - $reflMethod = new \ReflectionMethod(get_class($parentNode), '__load'); - $reflMethod->setAccessible(true); - - $reflMethod->invoke($parentNode); + if ($parentNode !== $node && $parentNode instanceof GhostObjectInterface) { + $parentNode->initializeProxy(); } // If tree is already locked, we throw an exception - $lockTimeProp = $meta->getReflectionProperty($config['lock_time']); - $lockTimeProp->setAccessible(true); - $lockTime = $lockTimeProp->getValue($parentNode); + $lockTime = $meta->getFieldValue($parentNode, $config['lock_time']); - if (!is_null($lockTime)) { - $lockTime = $lockTime instanceof \MongoDate ? $lockTime->sec : $lockTime->getTimestamp(); + if (null !== $lockTime) { + $lockTime = $lockTime instanceof UTCDateTime ? $lockTime->toDateTime()->getTimestamp() : $lockTime->getTimestamp(); } - if (!is_null($lockTime) && ($lockTime >= (time() - $config['locking_timeout']))) { + if (null !== $lockTime && ($lockTime >= (time() - $config['locking_timeout']))) { $msg = 'Tree with root id "%s" is locked.'; $id = $meta->getIdentifierValue($parentNode); throw new TreeLockingException(sprintf($msg, $id)); } - $this->rootsOfTreesWhichNeedsLocking[spl_object_hash($parentNode)] = $parentNode; + $this->rootsOfTreesWhichNeedsLocking[spl_object_id($parentNode)] = $parentNode; - $oid = spl_object_hash($node); + $oid = spl_object_id($node); switch ($action) { case self::ACTION_INSERT: @@ -429,90 +393,82 @@ public function processPreLockingActions($om, $node, $action) /** * Process pre-locking actions * - * @param ObjectManager $om - * @param AdapterInterface $ea - * @param object $node - * @param string $action + * @param object $node + * @param string $action * * @return void */ public function processPostEventsActions(ObjectManager $om, AdapterInterface $ea, $node, $action) { $meta = $om->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($om, $meta->name); + $config = $this->listener->getConfiguration($om, $meta->getName()); if ($config['activate_locking']) { switch ($action) { case self::ACTION_INSERT: - unset($this->pendingObjectsToInsert[spl_object_hash($node)]); + unset($this->pendingObjectsToInsert[spl_object_id($node)]); break; case self::ACTION_UPDATE: - unset($this->pendingObjectsToUpdate[spl_object_hash($node)]); + unset($this->pendingObjectsToUpdate[spl_object_id($node)]); break; case self::ACTION_REMOVE: - unset($this->pendingObjectsToRemove[spl_object_hash($node)]); + unset($this->pendingObjectsToRemove[spl_object_id($node)]); break; default: throw new \InvalidArgumentException(sprintf('"%s" is not a valid action.', $action)); } - if (empty($this->pendingObjectsToInsert) && empty($this->pendingObjectsToUpdate) && - empty($this->pendingObjectsToRemove)) { + if (empty($this->pendingObjectsToInsert) && empty($this->pendingObjectsToUpdate) + && empty($this->pendingObjectsToRemove)) { $this->releaseTreeLocks($om, $ea); } } } /** - * Locks all needed trees + * Remove node and its children * - * @param ObjectManager $om - * @param AdapterInterface $ea + * @param ObjectManager $om + * @param ClassMetadata $meta Metadata + * @param array $config config + * @param object $node node to remove * * @return void */ - protected function lockTrees(ObjectManager $om, AdapterInterface $ea) - { - // Do nothing by default - } + abstract public function removeNode($om, $meta, $config, $node); /** - * Releases all trees which are locked + * Returns children of the node with its original path * - * @param ObjectManager $om - * @param AdapterInterface $ea + * @param ObjectManager $om + * @param ClassMetadata $meta Metadata + * @param array $config config + * @param string $originalPath original path of object * - * @return void + * @return array|\Traversable */ - protected function releaseTreeLocks(ObjectManager $om, AdapterInterface $ea) - { - // Do nothing by default - } + abstract public function getChildren($om, $meta, $config, $originalPath); /** - * Remove node and its children - * - * @param ObjectManager $om - * @param object $meta - Metadata - * @param object $config - config - * @param object $node - node to remove + * Locks all needed trees * * @return void */ - abstract public function removeNode($om, $meta, $config, $node); + protected function lockTrees(ObjectManager $om, AdapterInterface $ea) + { + // Do nothing by default + } /** - * Returns children of the node with its original path - * - * @param ObjectManager $om - * @param object $meta - Metadata - * @param object $config - config - * @param string $originalPath - original path of object + * Releases all trees which are locked * - * @return array|\Traversable + * @return void */ - abstract public function getChildren($om, $meta, $config, $originalPath); + protected function releaseTreeLocks(ObjectManager $om, AdapterInterface $ea) + { + // Do nothing by default + } } diff --git a/lib/Gedmo/Tree/Strategy/ODM/MongoDB/MaterializedPath.php b/src/Tree/Strategy/ODM/MongoDB/MaterializedPath.php similarity index 58% rename from lib/Gedmo/Tree/Strategy/ODM/MongoDB/MaterializedPath.php rename to src/Tree/Strategy/ODM/MongoDB/MaterializedPath.php index e5ab019165..9e54b1fb37 100644 --- a/lib/Gedmo/Tree/Strategy/ODM/MongoDB/MaterializedPath.php +++ b/src/Tree/Strategy/ODM/MongoDB/MaterializedPath.php @@ -1,23 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Strategy\ODM\MongoDB; -use Gedmo\Tree\Strategy\AbstractMaterializedPath; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Tool\Wrapper\AbstractWrapper; +use Gedmo\Tree\Strategy\AbstractMaterializedPath; +use MongoDB\BSON\Regex; +use MongoDB\BSON\UTCDateTime; /** * This strategy makes tree using materialized path strategy * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ class MaterializedPath extends AbstractMaterializedPath { /** - * {@inheritdoc} + * @param DocumentManager $om + * @param ClassMetadata $meta */ public function removeNode($om, $meta, $config, $node) { @@ -26,10 +37,10 @@ public function removeNode($om, $meta, $config, $node) // Remove node's children $results = $om->createQueryBuilder() - ->find($meta->name) - ->field($config['path'])->equals(new \MongoRegex('/^'.preg_quote($wrapped->getPropertyValue($config['path'])).'.?+/')) + ->find($meta->getName()) + ->field($config['path'])->equals(new Regex('^'.preg_quote($wrapped->getPropertyValue($config['path'])).'.?+')) ->getQuery() - ->execute(); + ->getIterator(); foreach ($results as $node) { $uow->scheduleForDelete($node); @@ -37,42 +48,38 @@ public function removeNode($om, $meta, $config, $node) } /** - * {@inheritdoc} + * @param DocumentManager $om + * @param ClassMetadata $meta */ public function getChildren($om, $meta, $config, $originalPath) { return $om->createQueryBuilder() - ->find($meta->name) - ->field($config['path'])->equals(new \MongoRegex('/^'.preg_quote($originalPath).'.+/')) + ->find($meta->getName()) + ->field($config['path'])->equals(new Regex('^'.preg_quote($originalPath).'.+')) ->sort($config['path'], 'asc') // This may save some calls to updateNode ->getQuery() - ->execute(); + ->getIterator(); } /** - * {@inheritdoc} + * @param DocumentManager $om */ protected function lockTrees(ObjectManager $om, AdapterInterface $ea) { $uow = $om->getUnitOfWork(); - foreach ($this->rootsOfTreesWhichNeedsLocking as $oid => $root) { + foreach ($this->rootsOfTreesWhichNeedsLocking as $root) { $meta = $om->getClassMetadata(get_class($root)); - $config = $this->listener->getConfiguration($om, $meta->name); - $lockTimeProp = $meta->getReflectionProperty($config['lock_time']); - $lockTimeProp->setAccessible(true); - $lockTimeValue = new \MongoDate(); - $lockTimeProp->setValue($root, $lockTimeValue); - $changes = array( - $config['lock_time'] => array(null, $lockTimeValue), - ); + $config = $this->listener->getConfiguration($om, $meta->getName()); + $lockTimeValue = new UTCDateTime(); + $meta->setFieldValue($root, $config['lock_time'], $lockTimeValue); $ea->recomputeSingleObjectChangeSet($uow, $meta, $root); } } /** - * {@inheritdoc} + * @param DocumentManager $om */ protected function releaseTreeLocks(ObjectManager $om, AdapterInterface $ea) { @@ -80,14 +87,9 @@ protected function releaseTreeLocks(ObjectManager $om, AdapterInterface $ea) foreach ($this->rootsOfTreesWhichNeedsLocking as $oid => $root) { $meta = $om->getClassMetadata(get_class($root)); - $config = $this->listener->getConfiguration($om, $meta->name); - $lockTimeProp = $meta->getReflectionProperty($config['lock_time']); - $lockTimeProp->setAccessible(true); + $config = $this->listener->getConfiguration($om, $meta->getName()); $lockTimeValue = null; - $lockTimeProp->setValue($root, $lockTimeValue); - $changes = array( - $config['lock_time'] => array(null, null), - ); + $meta->setFieldValue($root, $config['lock_time'], $lockTimeValue); $ea->recomputeSingleObjectChangeSet($uow, $meta, $root); diff --git a/src/Tree/Strategy/ORM/Closure.php b/src/Tree/Strategy/ORM/Closure.php new file mode 100644 index 0000000000..c17c8202b1 --- /dev/null +++ b/src/Tree/Strategy/ORM/Closure.php @@ -0,0 +1,620 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree\Strategy\ORM; + +use Doctrine\DBAL\ArrayParameterType; +use Doctrine\Deprecations\Deprecation; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\AssociationMapping; +use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata; +use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessorFactory; +use Doctrine\ORM\Mapping\ToOneOwningSideMapping; +use Doctrine\ORM\Query; +use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\RuntimeException; +use Gedmo\Exception\UnexpectedValueException; +use Gedmo\Mapping\Event\AdapterInterface; +use Gedmo\Tool\Wrapper\AbstractWrapper; +use Gedmo\Tree\Node; +use Gedmo\Tree\Strategy; +use Gedmo\Tree\TreeListener; +use Psr\Cache\CacheItemPoolInterface; + +/** + * This strategy makes tree act like + * a closure table. + * + * @author Gustavo Adrian + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class Closure implements Strategy +{ + /** + * TreeListener + * + * @var TreeListener + */ + protected $listener; + + /** + * List of pending Nodes, which needs to + * be post processed because of having a parent Node + * which requires some additional calculations + * + * @var array> + */ + private array $pendingChildNodeInserts = []; + + /** + * List of nodes which has their parents updated, but using + * new nodes. They have to wait until their parents are inserted + * on DB to make the update + * + * @var array> + * + * @phpstan-var array + */ + private array $pendingNodeUpdates = []; + + /** + * List of pending Nodes, which needs their "level" + * field value set + * + * @var array + * + * @phpstan-var array + */ + private array $pendingNodesLevelProcess = []; + + public function __construct(TreeListener $listener) + { + $this->listener = $listener; + } + + public function getName() + { + return Strategy::CLOSURE; + } + + /** + * @param EntityManagerInterface $em + * @param ORMClassMetadata $meta + */ + public function processMetadataLoad($em, $meta) + { + // TODO: Remove the body of this method in the next major version. + $config = $this->listener->getConfiguration($em, $meta->getName()); + $closureMetadata = $em->getClassMetadata($config['closure']); + + $cmf = $em->getMetadataFactory(); + + $hasTheUserExplicitlyDefinedMapping = true; + + if (!$closureMetadata->hasAssociation('ancestor')) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2390', + 'Not adding mapping explicitly to "ancestor" property in "%s" is deprecated and will not work in' + .' version 4.0. You MUST explicitly set the mapping as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table', + $closureMetadata->getName() + ); + + $hasTheUserExplicitlyDefinedMapping = false; + + // create ancestor mapping + $ancestorMapping = [ + 'fieldName' => 'ancestor', + 'id' => false, + 'joinColumns' => [ + [ + 'name' => 'ancestor', + 'referencedColumnName' => 'id', + 'unique' => false, + 'nullable' => false, + 'onDelete' => 'CASCADE', + 'onUpdate' => null, + 'columnDefinition' => null, + ], + ], + 'inversedBy' => null, + 'targetEntity' => $meta->getName(), + 'cascade' => null, + 'fetch' => ORMClassMetadata::FETCH_LAZY, + ]; + $closureMetadata->mapManyToOne($ancestorMapping); + + if (property_exists($closureMetadata, 'propertyAccessors')) { + // ORM 3.4+ + $closureMetadata->propertyAccessors['ancestor'] = PropertyAccessorFactory::createPropertyAccessor( + $closureMetadata->getName(), + 'ancestor' + ); + } else { + // ORM 3.3- + $closureMetadata->reflFields['ancestor'] = $cmf + ->getReflectionService() + ->getAccessibleProperty($closureMetadata->getName(), 'ancestor') + ; + } + } + + if (!$closureMetadata->hasAssociation('descendant')) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2390', + 'Not adding mapping explicitly to "descendant" property in "%s" is deprecated and will not work in' + .' version 4.0. You MUST explicitly set the mapping as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table', + $closureMetadata->getName() + ); + + $hasTheUserExplicitlyDefinedMapping = false; + + // create descendant mapping + $descendantMapping = [ + 'fieldName' => 'descendant', + 'id' => false, + 'joinColumns' => [ + [ + 'name' => 'descendant', + 'referencedColumnName' => 'id', + 'unique' => false, + 'nullable' => false, + 'onDelete' => 'CASCADE', + 'onUpdate' => null, + 'columnDefinition' => null, + ], + ], + 'inversedBy' => null, + 'targetEntity' => $meta->getName(), + 'cascade' => null, + 'fetch' => ORMClassMetadata::FETCH_LAZY, + ]; + $closureMetadata->mapManyToOne($descendantMapping); + + if (property_exists($closureMetadata, 'propertyAccessors')) { + // ORM 3.4+ + $closureMetadata->propertyAccessors['descendant'] = PropertyAccessorFactory::createPropertyAccessor( + $closureMetadata->getName(), + 'descendant' + ); + } else { + // ORM 3.3- + $closureMetadata->reflFields['descendant'] = $cmf + ->getReflectionService() + ->getAccessibleProperty($closureMetadata->getName(), 'descendant') + ; + } + } + + if (!$this->hasClosureTableUniqueConstraint($closureMetadata)) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2390', + 'Not adding a unique constraint explicitly to "%s" is deprecated and will not be automatically' + .' added in version 4.0. You SHOULD explicitly add the unique constraint as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table', + $closureMetadata->getName() + ); + + $hasTheUserExplicitlyDefinedMapping = false; + + // create unique index on ancestor and descendant + $indexName = substr(strtoupper('IDX_'.md5($closureMetadata->getName())), 0, 20); + + $ancestorAssociationMapping = $em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor'); + $descendantAssociationMapping = $em->getClassMetadata($config['closure'])->getAssociationMapping('descendant'); + + $closureMetadata->table['uniqueConstraints'][$indexName] = [ + 'columns' => [ + $this->getJoinColumnFieldName(is_array($ancestorAssociationMapping) ? $ancestorAssociationMapping : clone $ancestorAssociationMapping), + $this->getJoinColumnFieldName(is_array($descendantAssociationMapping) ? $descendantAssociationMapping : clone $descendantAssociationMapping), + ], + ]; + } + + if (!$this->hasClosureTableDepthIndex($closureMetadata)) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2390', + 'Not adding an index with "depth" column explicitly to "%s" is deprecated and will not be automatically' + .' added in version 4.0. You SHOULD explicitly add the index as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table', + $closureMetadata->getName() + ); + + $hasTheUserExplicitlyDefinedMapping = false; + + // this one may not be very useful + $indexName = substr(strtoupper('IDX_'.md5($meta->getName().'depth')), 0, 20); + $closureMetadata->table['indexes'][$indexName] = [ + 'columns' => ['depth'], + ]; + } + + if (!$hasTheUserExplicitlyDefinedMapping) { + $metadataFactory = $em->getMetadataFactory(); + $getCache = \Closure::bind(static fn (AbstractClassMetadataFactory $metadataFactory): ?CacheItemPoolInterface => $metadataFactory->getCache(), null, \get_class($metadataFactory)); + + $metadataCache = $getCache($metadataFactory); + + if (null !== $metadataCache) { + // @see https://github.com/doctrine/persistence/pull/144 + // @see \Doctrine\Persistence\Mapping\AbstractClassMetadataFactory::getCacheKey() + $cacheKey = str_replace('\\', '__', $closureMetadata->getName()).'__CLASSMETADATA__'; + + $item = $metadataCache->getItem($cacheKey); + + $metadataCache->save($item->set($closureMetadata)); + } + } + } + + public function onFlushEnd($em, AdapterInterface $ea) + { + } + + public function processPrePersist($em, $node) + { + $this->pendingChildNodeInserts[spl_object_id($em)][spl_object_id($node)] = $node; + } + + public function processPreUpdate($em, $node) + { + } + + public function processPreRemove($em, $node) + { + } + + public function processScheduledInsertion($em, $node, AdapterInterface $ea) + { + } + + public function processScheduledDelete($em, $entity) + { + } + + public function processPostUpdate($em, $entity, AdapterInterface $ea) + { + \assert($em instanceof EntityManagerInterface); + $meta = $em->getClassMetadata(get_class($entity)); + $config = $this->listener->getConfiguration($em, $meta->getName()); + + // Process TreeLevel field value + if (!empty($config)) { + $this->setLevelFieldOnPendingNodes($em); + } + } + + public function processPostRemove($em, $entity, AdapterInterface $ea) + { + } + + /** + * @param EntityManagerInterface $em + */ + public function processPostPersist($em, $entity, AdapterInterface $ea) + { + $uow = $em->getUnitOfWork(); + $emHash = spl_object_id($em); + + while ($node = array_shift($this->pendingChildNodeInserts[$emHash])) { + $meta = $em->getClassMetadata(get_class($node)); + $config = $this->listener->getConfiguration($em, $meta->getName()); + + $identifier = $meta->getSingleIdentifierFieldName(); + $nodeId = $meta->getFieldValue($node, $identifier); + $parent = $meta->getFieldValue($node, $config['parent']); + + $closureClass = $config['closure']; + $closureMeta = $em->getClassMetadata($closureClass); + $closureTable = $closureMeta->getTableName(); + + $ancestorAssociationMapping = $em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor'); + $descendantAssociationMapping = $em->getClassMetadata($config['closure'])->getAssociationMapping('descendant'); + + $ancestorColumnName = $this->getJoinColumnFieldName(is_array($ancestorAssociationMapping) ? $ancestorAssociationMapping : clone $ancestorAssociationMapping); + $descendantColumnName = $this->getJoinColumnFieldName(is_array($descendantAssociationMapping) ? $descendantAssociationMapping : clone $descendantAssociationMapping); + $depthColumnName = $em->getClassMetadata($config['closure'])->getColumnName('depth'); + + $entries = [ + [ + $ancestorColumnName => $nodeId, + $descendantColumnName => $nodeId, + $depthColumnName => 0, + ], + ]; + + if ($parent) { + $dql = "SELECT c, a FROM {$closureMeta->getName()} c"; + $dql .= ' JOIN c.ancestor a'; + $dql .= ' WHERE c.descendant = :parent'; + $q = $em->createQuery($dql); + $q->setParameter('parent', $parent); + + $mustPostpone = true; + + foreach ($q->toIterable([], Query::HYDRATE_ARRAY) as $ancestor) { + $mustPostpone = false; + + $entries[] = [ + $ancestorColumnName => $ancestor['ancestor'][$identifier], + $descendantColumnName => $nodeId, + $depthColumnName => $ancestor['depth'] + 1, + ]; + } + + if ($mustPostpone) { + // The parent has been persisted after the child, postpone the evaluation + $this->pendingChildNodeInserts[$emHash][] = $node; + + continue; + } + + if (isset($config['level'])) { + $this->pendingNodesLevelProcess[$nodeId] = $node; + } + } elseif (isset($config['level'])) { + $uow->scheduleExtraUpdate($node, [$config['level'] => [null, 1]]); + $ea->setOriginalObjectProperty($uow, $node, $config['level'], 1); + $meta->setFieldValue($node, $config['level'], 1); + } + + foreach ($entries as $closure) { + if (!$em->getConnection()->insert($closureTable, $closure)) { + throw new RuntimeException('Failed to insert new Closure record'); + } + } + } + + // Process pending node updates + if (!empty($this->pendingNodeUpdates)) { + foreach ($this->pendingNodeUpdates as $info) { + $this->updateNode($em, $info['node'], $info['oldParent']); + } + + $this->pendingNodeUpdates = []; + } + + // Process TreeLevel field value + $this->setLevelFieldOnPendingNodes($em); + } + + /** + * @param EntityManagerInterface $em + */ + public function processScheduledUpdate($em, $node, AdapterInterface $ea) + { + $meta = $em->getClassMetadata(get_class($node)); + $config = $this->listener->getConfiguration($em, $meta->getName()); + $uow = $em->getUnitOfWork(); + $changeSet = $uow->getEntityChangeSet($node); + + if (array_key_exists($config['parent'], $changeSet)) { + // If new parent is new, we need to delay the update of the node + // until it is inserted on DB + $parent = $changeSet[$config['parent']][1] ? AbstractWrapper::wrap($changeSet[$config['parent']][1], $em) : null; + + if ($parent && !$parent->getIdentifier()) { + $this->pendingNodeUpdates[spl_object_id($node)] = [ + 'node' => $node, + 'oldParent' => $changeSet[$config['parent']][0], + ]; + } else { + $this->updateNode($em, $node, $changeSet[$config['parent']][0]); + } + } + } + + /** + * Update node and closures + * + * @param object $node + * @param object $oldParent + * + * @return void + */ + public function updateNode(EntityManagerInterface $em, $node, $oldParent) + { + $wrapped = AbstractWrapper::wrap($node, $em); + $meta = $wrapped->getMetadata(); + $config = $this->listener->getConfiguration($em, $meta->getName()); + $closureMeta = $em->getClassMetadata($config['closure']); + + $nodeId = $wrapped->getIdentifier(); + $parent = $wrapped->getPropertyValue($config['parent']); + $table = $closureMeta->getTableName(); + $conn = $em->getConnection(); + // ensure integrity + if ($parent) { + $dql = "SELECT COUNT(c) FROM {$closureMeta->getName()} c"; + $dql .= ' WHERE c.ancestor = :node'; + $dql .= ' AND c.descendant = :parent'; + $q = $em->createQuery($dql); + $q->setParameters([ + 'node' => $node, + 'parent' => $parent, + ]); + if ($q->getSingleScalarResult()) { + throw new UnexpectedValueException("Cannot set child as parent to node: {$nodeId}"); + } + } + + if ($oldParent) { + $subQuery = "SELECT c2.id FROM {$table} c1"; + $subQuery .= " JOIN {$table} c2 ON c1.descendant = c2.descendant"; + $subQuery .= ' WHERE c1.ancestor = :nodeId AND c2.depth > c1.depth'; + + $ids = $conn->executeQuery($subQuery, ['nodeId' => $nodeId])->fetchFirstColumn(); + if ([] !== $ids) { + // using subquery directly, sqlite acts unfriendly + $query = "DELETE FROM {$table} WHERE id IN (".implode(', ', $ids).')'; + if (0 === $conn->executeStatement($query)) { + throw new RuntimeException('Failed to remove old closures'); + } + } + } + + if ($parent) { + $wrappedParent = AbstractWrapper::wrap($parent, $em); + $parentId = $wrappedParent->getIdentifier(); + $query = 'SELECT c1.ancestor, c2.descendant, (c1.depth + c2.depth + 1) AS depth'; + $query .= " FROM {$table} c1, {$table} c2"; + $query .= ' WHERE c1.descendant = :parentId'; + $query .= ' AND c2.ancestor = :nodeId'; + + $closures = $conn->executeQuery($query, ['nodeId' => $nodeId, 'parentId' => $parentId])->fetchAllAssociative(); + + foreach ($closures as $closure) { + if (!$conn->insert($table, $closure)) { + throw new RuntimeException('Failed to insert new Closure record'); + } + } + } + + if (isset($config['level'])) { + $this->pendingNodesLevelProcess[$nodeId] = $node; + } + } + + /** + * @param array|AssociationMapping $association + * + * @return string|null + */ + protected function getJoinColumnFieldName($association) + { + if (is_array($association)) { + if (count($association['joinColumnFieldNames']) > 1) { + throw new RuntimeException('More association on field '.$association['fieldName']); + } + + return array_shift($association['joinColumnFieldNames']); + } + + if ($association instanceof ToOneOwningSideMapping) { + if (count($association->joinColumnFieldNames) > 1) { + throw new RuntimeException('More association on field '.$association->fieldName); + } + + return array_shift($association->joinColumnFieldNames); + } + + throw new RuntimeException('Unsupported mapping type '.gettype($association)); + } + + /** + * Process pending entities to set their "level" value + * + * @param EntityManagerInterface $em + * + * @return void + */ + protected function setLevelFieldOnPendingNodes(ObjectManager $em) + { + if (!empty($this->pendingNodesLevelProcess)) { + $first = array_slice($this->pendingNodesLevelProcess, 0, 1); + $first = array_shift($first); + + assert(null !== $first); + + $meta = $em->getClassMetadata(get_class($first)); + unset($first); + $identifier = $meta->getIdentifier(); + $mapping = $meta->getFieldMapping($identifier[0]); + $config = $this->listener->getConfiguration($em, $meta->getName()); + $closureClass = $config['closure']; + $closureMeta = $em->getClassMetadata($closureClass); + $uow = $em->getUnitOfWork(); + + foreach ($this->pendingNodesLevelProcess as $node) { + $children = $em->getRepository($meta->getName())->children($node); + + foreach ($children as $child) { + $this->pendingNodesLevelProcess[AbstractWrapper::wrap($child, $em)->getIdentifier()] = $child; + } + } + + // Avoid type conversion performance penalty + $type = 'integer' === ($mapping->type ?? $mapping['type']) + ? ArrayParameterType::INTEGER + : ArrayParameterType::STRING; + + // We calculate levels for all nodes + $sql = 'SELECT c.descendant, MAX(c.depth) + 1 AS levelNum '; + $sql .= 'FROM '.$closureMeta->getTableName().' c '; + $sql .= 'WHERE c.descendant IN (?) '; + $sql .= 'GROUP BY c.descendant'; + + $levelsAssoc = $em->getConnection()->executeQuery($sql, [array_keys($this->pendingNodesLevelProcess)], [$type])->fetchAllNumeric(); + + // create key pair array with resultset + $levels = []; + foreach ($levelsAssoc as $level) { + $levels[$level[0]] = $level[1]; + } + $levelsAssoc = null; + + // Now we update levels + foreach ($this->pendingNodesLevelProcess as $nodeId => $node) { + // Update new level + $level = $levels[$nodeId]; + $uow->scheduleExtraUpdate( + $node, + [$config['level'] => [ + $meta->getFieldValue($node, $config['level']), $level, + ]] + ); + $meta->setFieldValue($node, $config['level'], $level); + $uow->setOriginalEntityProperty(spl_object_id($node), $config['level'], $level); + } + + $this->pendingNodesLevelProcess = []; + } + } + + /** + * @param ORMClassMetadata $closureMetadata + */ + private function hasClosureTableUniqueConstraint(ClassMetadata $closureMetadata): bool + { + if (!isset($closureMetadata->table['uniqueConstraints'])) { + return false; + } + + foreach ($closureMetadata->table['uniqueConstraints'] as $uniqueConstraint) { + if ([] === array_diff(['ancestor', 'descendant'], $uniqueConstraint['columns'])) { + return true; + } + } + + return false; + } + + /** + * @param ORMClassMetadata $closureMetadata + */ + private function hasClosureTableDepthIndex(ClassMetadata $closureMetadata): bool + { + if (!isset($closureMetadata->table['indexes'])) { + return false; + } + + foreach ($closureMetadata->table['indexes'] as $uniqueConstraint) { + if ([] === array_diff(['depth'], $uniqueConstraint['columns'])) { + return true; + } + } + + return false; + } +} diff --git a/lib/Gedmo/Tree/Strategy/ORM/MaterializedPath.php b/src/Tree/Strategy/ORM/MaterializedPath.php similarity index 54% rename from lib/Gedmo/Tree/Strategy/ORM/MaterializedPath.php rename to src/Tree/Strategy/ORM/MaterializedPath.php index c7b25bbb63..20ecd85a47 100644 --- a/lib/Gedmo/Tree/Strategy/ORM/MaterializedPath.php +++ b/src/Tree/Strategy/ORM/MaterializedPath.php @@ -1,49 +1,71 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Strategy\ORM; -use Gedmo\Tree\Strategy\AbstractMaterializedPath; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; use Gedmo\Tool\Wrapper\AbstractWrapper; +use Gedmo\Tree\Strategy\AbstractMaterializedPath; /** * This strategy makes tree using materialized path strategy * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ class MaterializedPath extends AbstractMaterializedPath { /** - * {@inheritdoc} + * @param EntityManagerInterface $om + * @param ClassMetadata $meta */ public function removeNode($om, $meta, $config, $node) { - $uow = $om->getUnitOfWork(); $wrapped = AbstractWrapper::wrap($node, $om); $path = addcslashes($wrapped->getPropertyValue($config['path']), '%'); + $separator = $config['path_ends_with_separator'] ? null : $config['path_separator']; + // Remove node's children $qb = $om->createQueryBuilder(); $qb->select('e') ->from($config['useObjectClass'], 'e') - ->where($qb->expr()->like('e.'.$config['path'], $qb->expr()->literal($path.'%'))); + ->where($qb->expr()->like('e.'.$config['path'], $qb->expr()->literal($path.$separator.'%'))); + + if (isset($config['level'])) { + $lvlField = $config['level']; + $lvl = $wrapped->getPropertyValue($lvlField); + if (!empty($lvl)) { + $qb->andWhere($qb->expr()->gt('e.'.$lvlField, $qb->expr()->literal($lvl))); + } + } + $results = $qb->getQuery() - ->execute(); + ->toIterable(); foreach ($results as $node) { - $uow->scheduleForDelete($node); + $om->remove($node); } } /** - * {@inheritdoc} + * @param EntityManagerInterface $om + * @param ClassMetadata $meta */ public function getChildren($om, $meta, $config, $path) { $path = addcslashes($path, '%'); - $qb = $om->createQueryBuilder($config['useObjectClass']); + $qb = $om->createQueryBuilder(); $qb->select('e') ->from($config['useObjectClass'], 'e') ->where($qb->expr()->like('e.'.$config['path'], $qb->expr()->literal($path.'%'))) @@ -51,7 +73,6 @@ public function getChildren($om, $meta, $config, $path) ->orderBy('e.'.$config['path'], 'asc'); // This may save some calls to updateNode $qb->setParameter('path', $path); - return $qb->getQuery() - ->execute(); + return $qb->getQuery()->getResult(); } } diff --git a/lib/Gedmo/Tree/Strategy/ORM/Nested.php b/src/Tree/Strategy/ORM/Nested.php similarity index 52% rename from lib/Gedmo/Tree/Strategy/ORM/Nested.php rename to src/Tree/Strategy/ORM/Nested.php index 045ec176a5..bb491886d6 100644 --- a/lib/Gedmo/Tree/Strategy/ORM/Nested.php +++ b/src/Tree/Strategy/ORM/Nested.php @@ -1,14 +1,26 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Strategy\ORM; +use Doctrine\Common\Collections\Criteria; +use Doctrine\Deprecations\Deprecation; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\Persistence\Proxy; +use Gedmo\Exception\InvalidArgumentException; use Gedmo\Exception\UnexpectedValueException; -use Doctrine\ORM\Proxy\Proxy; +use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Tool\Wrapper\AbstractWrapper; +use Gedmo\Tree\Node; use Gedmo\Tree\Strategy; -use Doctrine\ORM\EntityManager; use Gedmo\Tree\TreeListener; -use Gedmo\Mapping\Event\AdapterInterface; /** * This strategy makes the tree act like a nested set. @@ -17,72 +29,78 @@ * since nested set trees are slow on inserts and updates. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ class Nested implements Strategy { /** * Previous sibling position */ - const PREV_SIBLING = 'PrevSibling'; + public const PREV_SIBLING = 'PrevSibling'; /** * Next sibling position */ - const NEXT_SIBLING = 'NextSibling'; + public const NEXT_SIBLING = 'NextSibling'; /** - * Last child position + * First child position */ - const LAST_CHILD = 'LastChild'; + public const FIRST_CHILD = 'FirstChild'; /** - * First child position + * Last child position */ - const FIRST_CHILD = 'FirstChild'; + public const LAST_CHILD = 'LastChild'; + + public const ALLOWED_NODE_POSITIONS = [ + self::PREV_SIBLING, + self::NEXT_SIBLING, + self::FIRST_CHILD, + self::LAST_CHILD, + ]; /** * TreeListener * * @var TreeListener */ - protected $listener = null; + protected $listener; /** * The max number of "right" field of the * tree in case few root nodes will be persisted * on one flush for node classes * - * @var array + * @var array */ - private $treeEdges = array(); + private array $treeEdges = []; /** * Stores a list of node position strategies - * for each node by object hash + * for each node by object id * - * @var array + * @var array + * + * @phpstan-var array> */ - private $nodePositions = array(); + private array $nodePositions = []; /** * Stores a list of delayed nodes for correct order of updates * - * @var array + * @var array>> + * + * @phpstan-var array}>> */ - private $delayedNodes = array(); + private array $delayedNodes = []; - /** - * {@inheritdoc} - */ public function __construct(TreeListener $listener) { $this->listener = $listener; } - /** - * {@inheritdoc} - */ public function getName() { return Strategy::NESTED; @@ -91,61 +109,57 @@ public function getName() /** * Set node position strategy * - * @param string $oid + * @param int $oid * @param string $position + * + * @return void */ public function setNodePosition($oid, $position) { - $valid = array( - self::FIRST_CHILD, - self::LAST_CHILD, - self::NEXT_SIBLING, - self::PREV_SIBLING, - ); - if (!in_array($position, $valid, false)) { - throw new \Gedmo\Exception\InvalidArgumentException("Position: {$position} is not valid in nested set tree"); + if (!in_array($position, self::ALLOWED_NODE_POSITIONS, true)) { + throw new InvalidArgumentException("Position: {$position} is not valid in nested set tree"); } $this->nodePositions[$oid] = $position; } - /** - * {@inheritdoc} - */ public function processScheduledInsertion($em, $node, AdapterInterface $ea) { + /** @var ClassMetadata $meta */ $meta = $em->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($em, $meta->name); + $config = $this->listener->getConfiguration($em, $meta->getName()); - $meta->getReflectionProperty($config['left'])->setValue($node, 0); - $meta->getReflectionProperty($config['right'])->setValue($node, 0); + $meta->setFieldValue($node, $config['left'], 0); + $meta->setFieldValue($node, $config['right'], 0); if (isset($config['level'])) { - $meta->getReflectionProperty($config['level'])->setValue($node, 0); + $meta->setFieldValue($node, $config['level'], 0); } - if (isset($config['root'])) { - $meta->getReflectionProperty($config['root'])->setValue($node, 0); + if (isset($config['root']) && !$meta->hasAssociation($config['root']) && !isset($config['rootIdentifierMethod'])) { + $meta->setFieldValue($node, $config['root'], 0); + } elseif (isset($config['rootIdentifierMethod']) && null === $meta->getFieldValue($node, $config['root'])) { + $meta->setFieldValue($node, $config['root'], 0); } } /** - * {@inheritdoc} + * @param EntityManagerInterface $em */ public function processScheduledUpdate($em, $node, AdapterInterface $ea) { $meta = $em->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($em, $meta->name); + $config = $this->listener->getConfiguration($em, $meta->getName()); $uow = $em->getUnitOfWork(); $changeSet = $uow->getEntityChangeSet($node); - if (isset($config['root']) && isset($changeSet[$config['root']])) { - throw new \Gedmo\Exception\UnexpectedValueException("Root cannot be changed manually, change parent instead"); + if (isset($config['root'], $changeSet[$config['root']])) { + throw new UnexpectedValueException('Root cannot be changed manually, change parent instead'); } - $oid = spl_object_hash($node); - if (isset($changeSet[$config['left']]) && isset($this->nodePositions[$oid])) { + $oid = spl_object_id($node); + if (isset($changeSet[$config['left']], $this->nodePositions[$oid])) { $wrapped = AbstractWrapper::wrap($node, $em); $parent = $wrapped->getPropertyValue($config['parent']); // revert simulated changeset - $uow->clearEntityChangeSet($oid); + $ea->clearObjectChangeSet($uow, $node); $wrapped->setPropertyValue($config['left'], $changeSet[$config['left']][0]); $uow->setOriginalEntityProperty($oid, $config['left'], $changeSet[$config['left']][0]); // set back all other changes @@ -168,23 +182,24 @@ public function processScheduledUpdate($em, $node, AdapterInterface $ea) } /** - * {@inheritdoc} + * @param EntityManagerInterface $em */ public function processPostPersist($em, $node, AdapterInterface $ea) { $meta = $em->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($em, $meta->name); - $parent = $meta->getReflectionProperty($config['parent'])->getValue($node); + + $config = $this->listener->getConfiguration($em, $meta->getName()); + $parent = $meta->getFieldValue($node, $config['parent']); $this->updateNode($em, $node, $parent, self::LAST_CHILD); } /** - * {@inheritdoc} + * @param EntityManagerInterface $em */ public function processScheduledDelete($em, $node) { $meta = $em->getClassMetadata(get_class($node)); - $config = $this->listener->getConfiguration($em, $meta->name); + $config = $this->listener->getConfiguration($em, $meta->getName()); $uow = $em->getUnitOfWork(); $wrapped = AbstractWrapper::wrap($node, $em); @@ -201,8 +216,8 @@ public function processScheduledDelete($em, $node) $qb->select('node') ->from($config['useObjectClass'], 'node') ->where($qb->expr()->between('node.'.$config['left'], '?1', '?2')) - ->setParameters(array(1 => $leftValue, 2 => $rightValue)) - ; + ->setParameter(1, $leftValue) + ->setParameter(2, $rightValue); if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); @@ -210,83 +225,65 @@ public function processScheduledDelete($em, $node) } $q = $qb->getQuery(); // get nodes for deletion - $nodes = $q->getResult(); - foreach ((array) $nodes as $removalNode) { + foreach ($q->toIterable() as $removalNode) { $uow->scheduleForDelete($removalNode); } } $this->shiftRL($em, $config['useObjectClass'], $rightValue + 1, -$diff, $rootId); } - /** - * {@inheritdoc} - */ public function onFlushEnd($em, AdapterInterface $ea) { // reset values - $this->treeEdges = array(); + $this->treeEdges = []; } - /** - * {@inheritdoc} - */ public function processPreRemove($em, $node) { } - /** - * {@inheritdoc} - */ public function processPrePersist($em, $node) { } - /** - * {@inheritdoc} - */ public function processPreUpdate($em, $node) { } - /** - * {@inheritdoc} - */ public function processMetadataLoad($em, $meta) { } - /** - * {@inheritdoc} - */ public function processPostUpdate($em, $entity, AdapterInterface $ea) { } - /** - * {@inheritdoc} - */ public function processPostRemove($em, $entity, AdapterInterface $ea) { } /** - * Update the $node with a diferent $parent - * destination + * Update the $node with a different $parent destination + * + * @param Node|object $node target node + * @param Node|object $parent destination node + * @param string $position * - * @param EntityManager $em - * @param object $node - target node - * @param object $parent - destination node - * @param string $position + * @phpstan-param value-of $position * - * @throws \Gedmo\Exception\UnexpectedValueException + * @throws UnexpectedValueException + * + * @return void */ - public function updateNode(EntityManager $em, $node, $parent, $position = 'FirstChild') + public function updateNode(EntityManagerInterface $em, $node, $parent, $position = self::FIRST_CHILD) { $wrapped = AbstractWrapper::wrap($node, $em); + + /** @var ClassMetadata $meta */ $meta = $wrapped->getMetadata(); - $config = $this->listener->getConfiguration($em, $meta->name); + $config = $this->listener->getConfiguration($em, $meta->getName()); - $rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null; + $root = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null; $identifierField = $meta->getSingleIdentifierFieldName(); $nodeId = $wrapped->getIdentifier(); @@ -299,31 +296,43 @@ public function updateNode(EntityManager $em, $node, $parent, $position = 'First $right = 2; } - $oid = spl_object_hash($node); + $oid = spl_object_id($node); if (isset($this->nodePositions[$oid])) { $position = $this->nodePositions[$oid]; } - $level = 0; + $level = $config['level_base'] ?? 0; $treeSize = $right - $left + 1; - $newRootId = null; - if ($parent) { + $newRoot = null; + + // @todo: In the next major release, remove all the conditions and use only the following assignment for `$sibling`. + // $node->getSibling(); + + if (method_exists($node, 'getSibling')) { + $sibling = $node->getSibling(); + } elseif (property_exists($node, 'sibling')) { + $sibling = $node->sibling; + } else { + $sibling = null; + } + + if ($parent) { // || (!$parent && isset($config['rootIdentifierMethod'])) $wrappedParent = AbstractWrapper::wrap($parent, $em); - $parentRootId = isset($config['root']) ? $wrappedParent->getPropertyValue($config['root']) : null; - $parentOid = spl_object_hash($parent); + $parentRoot = isset($config['root']) ? $wrappedParent->getPropertyValue($config['root']) : null; + $parentOid = spl_object_id($parent); $parentLeft = $wrappedParent->getPropertyValue($config['left']); $parentRight = $wrappedParent->getPropertyValue($config['right']); if (empty($parentLeft) && empty($parentRight)) { // parent node is a new node, but wasn't processed yet (due to Doctrine commit order calculator redordering) // We delay processing of node to the moment parent node will be processed if (!isset($this->delayedNodes[$parentOid])) { - $this->delayedNodes[$parentOid] = array(); + $this->delayedNodes[$parentOid] = []; } - $this->delayedNodes[$parentOid][] = array('node' => $node, 'position' => $position); + $this->delayedNodes[$parentOid][] = ['node' => $node, 'position' => $position]; return; } - if (!$isNewNode && $rootId === $parentRootId && $parentLeft >= $left && $parentRight <= $right) { + if (!$isNewNode && $root === $parentRoot && $parentLeft >= $left && $parentRight <= $right) { throw new UnexpectedValueException("Cannot set child as parent to node: {$nodeId}"); } if (isset($config['level'])) { @@ -331,69 +340,157 @@ public function updateNode(EntityManager $em, $node, $parent, $position = 'First } switch ($position) { case self::PREV_SIBLING: - if (property_exists($node, 'sibling')) { - $wrappedSibling = AbstractWrapper::wrap($node->sibling, $em); + if (null !== $sibling) { + $wrappedSibling = AbstractWrapper::wrap($sibling, $em); $start = $wrappedSibling->getPropertyValue($config['left']); - $level++; + ++$level; } else { $newParent = $wrappedParent->getPropertyValue($config['parent']); - if (is_null($newParent) && (isset($config['root']) || $isNewNode)) { - throw new UnexpectedValueException("Cannot persist sibling for a root node, tree operation is not possible"); + + if (null === $newParent && ((isset($config['root']) && $config['root'] == $config['parent']) || $isNewNode)) { + throw new UnexpectedValueException('Cannot persist sibling for a root node, tree operation is not possible'); } - $wrapped->setPropertyValue($config['parent'], $newParent); + if (null === $newParent && (isset($config['root']) || $isNewNode)) { + // root is a different column from parent (pointing to another table?), do nothing + } else { + $wrapped->setPropertyValue($config['parent'], $newParent); + } + $em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node); $start = $parentLeft; } + break; case self::NEXT_SIBLING: - if (property_exists($node, 'sibling')) { - $wrappedSibling = AbstractWrapper::wrap($node->sibling, $em); + if (null !== $sibling) { + $wrappedSibling = AbstractWrapper::wrap($sibling, $em); $start = $wrappedSibling->getPropertyValue($config['right']) + 1; - $level++; + ++$level; } else { $newParent = $wrappedParent->getPropertyValue($config['parent']); - if (is_null($newParent) && (isset($config['root']) || $isNewNode)) { - throw new UnexpectedValueException("Cannot persist sibling for a root node, tree operation is not possible"); + if (null === $newParent && ((isset($config['root']) && $config['root'] == $config['parent']) || $isNewNode)) { + throw new UnexpectedValueException('Cannot persist sibling for a root node, tree operation is not possible'); + } + if (null === $newParent && (isset($config['root']) || $isNewNode)) { + // root is a different column from parent (pointing to another table?), do nothing + } else { + $wrapped->setPropertyValue($config['parent'], $newParent); } - $wrapped->setPropertyValue($config['parent'], $newParent); + $em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node); $start = $parentRight + 1; } + + break; + + case self::LAST_CHILD: + $start = $parentRight; + ++$level; + + break; + + case self::FIRST_CHILD: + default: + $start = $parentLeft + 1; + ++$level; + + break; + } + $this->shiftRL($em, $config['useObjectClass'], $start, $treeSize, $parentRoot); + if (!$isNewNode && $root === $parentRoot && $left >= $start) { + $left += $treeSize; + $wrapped->setPropertyValue($config['left'], $left); + } + if (!$isNewNode && $root === $parentRoot && $right >= $start) { + $right += $treeSize; + $wrapped->setPropertyValue($config['right'], $right); + } + $newRoot = $parentRoot; + } elseif (!isset($config['root']) + || ($meta->isSingleValuedAssociation($config['root']) && null !== $parent && ($newRoot = $meta->getFieldValue($node, $config['root'])))) { + if (!isset($this->treeEdges[$meta->getName()])) { + $this->treeEdges[$meta->getName()] = $this->max($em, $config['useObjectClass'], $newRoot) + 1; + } + + $level = 0; + $parentLeft = 0; + $parentRight = $this->treeEdges[$meta->getName()]; + $this->treeEdges[$meta->getName()] += 2; + + switch ($position) { + case self::PREV_SIBLING: + if (null !== $sibling) { + $wrappedSibling = AbstractWrapper::wrap($sibling, $em); + $start = $wrappedSibling->getPropertyValue($config['left']); + } else { + $wrapped->setPropertyValue($config['parent'], null); + $em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node); + $start = $parentLeft + 1; + } + + break; + + case self::NEXT_SIBLING: + if (null !== $sibling) { + $wrappedSibling = AbstractWrapper::wrap($sibling, $em); + $start = $wrappedSibling->getPropertyValue($config['right']) + 1; + } else { + $wrapped->setPropertyValue($config['parent'], null); + $em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node); + $start = $parentRight; + } + break; case self::LAST_CHILD: $start = $parentRight; - $level++; + break; case self::FIRST_CHILD: default: $start = $parentLeft + 1; - $level++; + break; } - $this->shiftRL($em, $config['useObjectClass'], $start, $treeSize, $parentRootId); - if (!$isNewNode && $rootId === $parentRootId && $left >= $start) { + + $this->shiftRL($em, $config['useObjectClass'], $start, $treeSize, null); + + if (!$isNewNode && $left >= $start) { $left += $treeSize; $wrapped->setPropertyValue($config['left'], $left); } - if (!$isNewNode && $rootId === $parentRootId && $right >= $start) { + if (!$isNewNode && $right >= $start) { $right += $treeSize; $wrapped->setPropertyValue($config['right'], $right); } - $newRootId = $parentRootId; - } elseif (!isset($config['root'])) { - $start = isset($this->treeEdges[$meta->name]) ? - $this->treeEdges[$meta->name] : $this->max($em, $config['useObjectClass']); - $this->treeEdges[$meta->name] = $start + 2; - $start++; } else { $start = 1; - $newRootId = $nodeId; + if (isset($config['rootIdentifierMethod'])) { + $method = $config['rootIdentifierMethod']; + $newRoot = $node->$method(); + $repo = $em->getRepository($config['useObjectClass']); + + $criteria = new Criteria(); + $criteria->andWhere(Criteria::expr()->notIn($wrapped->getMetadata()->getIdentifier()[0], [$wrapped->getIdentifier()])); + $criteria->andWhere(Criteria::expr()->eq($config['root'], $node->$method())); + $criteria->andWhere(Criteria::expr()->isNull($config['parent'])); + $criteria->andWhere(Criteria::expr()->eq($config['level'], 0)); + $criteria->orderBy([$config['right'] => Criteria::ASC]); + $roots = $repo->matching($criteria)->toArray(); + $last = array_pop($roots); + + $start = ($last) ? $meta->getFieldValue($last, $config['right']) + 1 : 1; + } elseif ($meta->isSingleValuedAssociation($config['root'])) { + $newRoot = $node; + } else { + $newRoot = $wrapped->getIdentifier(); + } } $diff = $start - $left; + if (!$isNewNode) { $levelDiff = isset($config['level']) ? $level - $wrapped->getPropertyValue($config['level']) : null; $this->shiftRangeRL( @@ -402,19 +499,19 @@ public function updateNode(EntityManager $em, $node, $parent, $position = 'First $left, $right, $diff, - $rootId, - $newRootId, + $root, + $newRoot, $levelDiff ); - $this->shiftRL($em, $config['useObjectClass'], $left, -$treeSize, $rootId); + $this->shiftRL($em, $config['useObjectClass'], $left, -$treeSize, $root); } else { $qb = $em->createQueryBuilder(); $qb->update($config['useObjectClass'], 'node'); if (isset($config['root'])) { $qb->set('node.'.$config['root'], ':rid'); - $qb->setParameter('rid', $newRootId); - $wrapped->setPropertyValue($config['root'], $newRootId); - $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['root'], $newRootId); + $qb->setParameter('rid', $newRoot); + $wrapped->setPropertyValue($config['root'], $newRoot); + $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['root'], $newRoot); } if (isset($config['level'])) { $qb->set('node.'.$config['level'], $level); @@ -450,20 +547,20 @@ public function updateNode(EntityManager $em, $node, $parent, $position = 'First /** * Get the edge of tree * - * @param EntityManager $em - * @param string $class - * @param integer $rootId + * @param string $class + * @param int $rootId * - * @return integer + * @phpstan-param class-string $class + * + * @return int */ - public function max(EntityManager $em, $class, $rootId = 0) + public function max(EntityManagerInterface $em, $class, $rootId = 0) { $meta = $em->getClassMetadata($class); - $config = $this->listener->getConfiguration($em, $meta->name); + $config = $this->listener->getConfiguration($em, $meta->getName()); $qb = $em->createQueryBuilder(); $qb->select($qb->expr()->max('node.'.$config['right'])) - ->from($config['useObjectClass'], 'node') - ; + ->from($config['useObjectClass'], 'node'); if (isset($config['root']) && $rootId) { $qb->where($qb->expr()->eq('node.'.$config['root'], ':rid')); @@ -472,19 +569,22 @@ public function max(EntityManager $em, $class, $rootId = 0) $query = $qb->getQuery(); $right = $query->getSingleScalarResult(); - return intval($right); + return (int) $right; } /** * Shift tree left and right values by delta * - * @param EntityManager $em - * @param string $class - * @param integer $first - * @param integer $delta - * @param integer|string $rootId + * @param string $class + * @param int $first + * @param int $delta + * @param int|string $root + * + * @phpstan-param class-string $class + * + * @return void */ - public function shiftRL(EntityManager $em, $class, $first, $delta, $rootId = null) + public function shiftRL(EntityManagerInterface $em, $class, $first, $delta, $root = null) { $meta = $em->getClassMetadata($class); $config = $this->listener->getConfiguration($em, $class); @@ -494,22 +594,20 @@ public function shiftRL(EntityManager $em, $class, $first, $delta, $rootId = nul $qb = $em->createQueryBuilder(); $qb->update($config['useObjectClass'], 'node') ->set('node.'.$config['left'], "node.{$config['left']} {$sign} {$absDelta}") - ->where($qb->expr()->gte('node.'.$config['left'], $first)) - ; + ->where($qb->expr()->gte('node.'.$config['left'], $first)); if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); + $qb->setParameter('rid', $root); } $qb->getQuery()->getSingleScalarResult(); $qb = $em->createQueryBuilder(); $qb->update($config['useObjectClass'], 'node') ->set('node.'.$config['right'], "node.{$config['right']} {$sign} {$absDelta}") - ->where($qb->expr()->gte('node.'.$config['right'], $first)) - ; + ->where($qb->expr()->gte('node.'.$config['right'], $first)); if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); + $qb->setParameter('rid', $root); } $qb->getQuery()->getSingleScalarResult(); @@ -520,19 +618,37 @@ public function shiftRL(EntityManager $em, $class, $first, $delta, $rootId = nul continue; } foreach ($nodes as $node) { - if ($node instanceof Proxy && !$node->__isInitialized__) { + if ($node instanceof Proxy && !$node->__isInitialized()) { continue; } - $oid = spl_object_hash($node); - $left = $meta->getReflectionProperty($config['left'])->getValue($node); - $root = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($node) : null; - if ($root === $rootId && $left >= $first) { - $meta->getReflectionProperty($config['left'])->setValue($node, $left + $delta); + + assert(null !== $node); + + $nodeMeta = $em->getClassMetadata(get_class($node)); + + /** @phpstan-ignore-next-line function.alreadyNarrowedType Property introduced in ORM 3.4 */ + if (property_exists($nodeMeta, 'propertyAccessors')) { + // ORM 3.4+ + if (!array_key_exists($config['left'], $nodeMeta->getPropertyAccessors())) { + continue; + } + } else { + // ORM 3.3- + if (!array_key_exists($config['left'], $nodeMeta->getReflectionProperties())) { + continue; + } + } + + $oid = spl_object_id($node); + $left = $meta->getFieldValue($node, $config['left']); + $currentRoot = isset($config['root']) ? $meta->getFieldValue($node, $config['root']) : null; + if ($currentRoot === $root && $left >= $first) { + $meta->setFieldValue($node, $config['left'], $left + $delta); $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['left'], $left + $delta); } - $right = $meta->getReflectionProperty($config['right'])->getValue($node); - if ($root === $rootId && $right >= $first) { - $meta->getReflectionProperty($config['right'])->setValue($node, $right + $delta); + $right = $meta->getFieldValue($node, $config['right']); + if ($currentRoot === $root && $right >= $first) { + $meta->setFieldValue($node, $config['right'], $right + $delta); $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['right'], $right + $delta); } } @@ -543,17 +659,34 @@ public function shiftRL(EntityManager $em, $class, $first, $delta, $rootId = nul * Shift range of right and left values on tree * depending on tree level difference also * - * @param EntityManager $em - * @param string $class - * @param integer $first - * @param integer $last - * @param integer $delta - * @param integer|string $rootId - * @param integer|string $destRootId - * @param integer $levelDelta + * @param string $class + * @param int $first + * @param int $last + * @param int $delta + * @param int|string $root + * @param int|string $destRoot + * @param int $levelDelta + * + * @phpstan-param class-string $class + * + * @return void */ - public function shiftRangeRL(EntityManager $em, $class, $first, $last, $delta, $rootId = null, $destRootId = null, $levelDelta = null) + public function shiftRangeRL(EntityManagerInterface $em, $class, $first, $last, $delta, $root = null, $destRoot = null, $levelDelta = null) { + // @todo: Remove the following condition and assignment in the next major release and use 0 as default value for + // the `$levelDelta` parameter. + if (null === $levelDelta && func_num_args() >= 8) { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2495', + 'Passing a type different than "int" as argument 8 to "%s()" is deprecated since gedmo/doctrine-extensions'. + ' 3.9 and will throw a "%s" error in version 4.0.', + __METHOD__, + \TypeError::class + ); + } + $levelDelta ??= 0; + $meta = $em->getClassMetadata($class); $config = $this->listener->getConfiguration($em, $class); @@ -567,13 +700,12 @@ public function shiftRangeRL(EntityManager $em, $class, $first, $last, $delta, $ ->set('node.'.$config['left'], "node.{$config['left']} {$sign} {$absDelta}") ->set('node.'.$config['right'], "node.{$config['right']} {$sign} {$absDelta}") ->where($qb->expr()->gte('node.'.$config['left'], $first)) - ->andWhere($qb->expr()->lte('node.'.$config['right'], $last)) - ; + ->andWhere($qb->expr()->lte('node.'.$config['right'], $last)); if (isset($config['root'])) { $qb->set('node.'.$config['root'], ':drid'); - $qb->setParameter('drid', $destRootId); + $qb->setParameter('drid', $destRoot); $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); - $qb->setParameter('rid', $rootId); + $qb->setParameter('rid', $root); } if (isset($config['level'])) { $qb->set('node.'.$config['level'], "node.{$config['level']} {$levelSign} {$absLevelDelta}"); @@ -586,27 +718,45 @@ public function shiftRangeRL(EntityManager $em, $class, $first, $last, $delta, $ continue; } foreach ($nodes as $node) { - if ($node instanceof Proxy && !$node->__isInitialized__) { + if ($node instanceof Proxy && !$node->__isInitialized()) { continue; } - $left = $meta->getReflectionProperty($config['left'])->getValue($node); - $right = $meta->getReflectionProperty($config['right'])->getValue($node); - $root = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($node) : null; - if ($root === $rootId && $left >= $first && $right <= $last) { - $oid = spl_object_hash($node); + + assert(null !== $node); + + $nodeMeta = $em->getClassMetadata(get_class($node)); + + /** @phpstan-ignore-next-line function.alreadyNarrowedType Property introduced in ORM 3.4 */ + if (property_exists($nodeMeta, 'propertyAccessors')) { + // ORM 3.4+ + if (!array_key_exists($config['left'], $nodeMeta->getPropertyAccessors())) { + continue; + } + } else { + // ORM 3.3- + if (!array_key_exists($config['left'], $nodeMeta->getReflectionProperties())) { + continue; + } + } + + $left = $meta->getFieldValue($node, $config['left']); + $right = $meta->getFieldValue($node, $config['right']); + $currentRoot = isset($config['root']) ? $meta->getFieldValue($node, $config['root']) : null; + if ($currentRoot === $root && $left >= $first && $right <= $last) { + $oid = spl_object_id($node); $uow = $em->getUnitOfWork(); - $meta->getReflectionProperty($config['left'])->setValue($node, $left + $delta); + $meta->setFieldValue($node, $config['left'], $left + $delta); $uow->setOriginalEntityProperty($oid, $config['left'], $left + $delta); - $meta->getReflectionProperty($config['right'])->setValue($node, $right + $delta); + $meta->setFieldValue($node, $config['right'], $right + $delta); $uow->setOriginalEntityProperty($oid, $config['right'], $right + $delta); if (isset($config['root'])) { - $meta->getReflectionProperty($config['root'])->setValue($node, $destRootId); - $uow->setOriginalEntityProperty($oid, $config['root'], $destRootId); + $meta->setFieldValue($node, $config['root'], $destRoot); + $uow->setOriginalEntityProperty($oid, $config['root'], $destRoot); } if (isset($config['level'])) { - $level = $meta->getReflectionProperty($config['level'])->getValue($node); - $meta->getReflectionProperty($config['level'])->setValue($node, $level + $levelDelta); + $level = $meta->getFieldValue($node, $config['level']); + $meta->setFieldValue($node, $config['level'], $level + $levelDelta); $uow->setOriginalEntityProperty($oid, $config['level'], $level + $levelDelta); } } diff --git a/lib/Gedmo/Tree/Traits/MaterializedPath.php b/src/Tree/Traits/MaterializedPath.php similarity index 68% rename from lib/Gedmo/Tree/Traits/MaterializedPath.php rename to src/Tree/Traits/MaterializedPath.php index 87ace1f144..9d6915122e 100644 --- a/lib/Gedmo/Tree/Traits/MaterializedPath.php +++ b/src/Tree/Traits/MaterializedPath.php @@ -1,15 +1,23 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree\Traits; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; /** - * MaterializedPath Trait + * Trait for objects in a materialized path tree. + * + * This implementation does not provide any mapping configurations. * * @author Steffen RoรŸkamp - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ trait MaterializedPath { @@ -17,29 +25,31 @@ trait MaterializedPath * @var string */ protected $path; + /** - * @var self + * @var self|null */ protected $parent; + /** - * @var integer + * @var int */ protected $level; + /** - * @var Collection|self[] + * @var Collection|self[]|null */ protected $children; + /** * @var string */ protected $hash; /** - * @param self $parent - * * @return self */ - public function setParent(self $parent = null) + public function setParent(?self $parent = null) { $this->parent = $parent; @@ -75,7 +85,7 @@ public function getPath() } /** - * @return integer + * @return int */ public function getLevel() { @@ -103,7 +113,7 @@ public function getHash() } /** - * @param Collection|self[] $children + * @param Collection|self[] $children * * @return self */ @@ -115,10 +125,10 @@ public function setChildren($children) } /** - * @return Collection|self[] + * @return Collection|self[] */ public function getChildren() { - return $this->children = $this->children ?: new ArrayCollection(); + return $this->children ??= new ArrayCollection(); } } diff --git a/src/Tree/Traits/NestedSet.php b/src/Tree/Traits/NestedSet.php new file mode 100644 index 0000000000..4d5f2e623a --- /dev/null +++ b/src/Tree/Traits/NestedSet.php @@ -0,0 +1,40 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree\Traits; + +/** + * Trait for objects in a nested tree. + * + * This implementation does not provide any mapping configurations. + * + * @author Renaat De Muynck + */ +trait NestedSet +{ + /** + * @var int + */ + private $root; + + /** + * @var int + */ + private $level; + + /** + * @var int + */ + private $left; + + /** + * @var int + */ + private $right; +} diff --git a/src/Tree/Traits/NestedSetEntity.php b/src/Tree/Traits/NestedSetEntity.php new file mode 100644 index 0000000000..a6fc8f265a --- /dev/null +++ b/src/Tree/Traits/NestedSetEntity.php @@ -0,0 +1,68 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree\Traits; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * Trait for objects in a nested tree. + * + * This implementation provides a mapping configuration for the Doctrine ORM for entities using numeric primary keys. + * + * @author Renaat De Muynck + */ +trait NestedSetEntity +{ + /** + * @var int + * + * @Gedmo\TreeRoot + * + * @ORM\Column(name="root", type="integer", nullable=true) + */ + #[ORM\Column(name: 'root', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeRoot] + private $root; + + /** + * @var int + * + * @Gedmo\TreeLevel + * + * @ORM\Column(name="lvl", type="integer") + */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private $level; + + /** + * @var int + * + * @Gedmo\TreeLeft + * + * @ORM\Column(name="lft", type="integer") + */ + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private $left; + + /** + * @var int + * + * @Gedmo\TreeRight + * + * @ORM\Column(name="rgt", type="integer") + */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] + private $right; +} diff --git a/src/Tree/Traits/NestedSetEntityUuid.php b/src/Tree/Traits/NestedSetEntityUuid.php new file mode 100644 index 0000000000..1e038fdd77 --- /dev/null +++ b/src/Tree/Traits/NestedSetEntityUuid.php @@ -0,0 +1,37 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tree\Traits; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * Trait for objects in a nested tree. + * + * This implementation provides a mapping configuration for the Doctrine ORM for entities using UUID-generated primary keys. + * + * @author Benjamin Lazarecki + */ +trait NestedSetEntityUuid +{ + use NestedSetEntity; + + /** + * @var string + * + * @Gedmo\TreeRoot + * + * @ORM\Column(name="root", type="string", nullable=true) + */ + #[ORM\Column(name: 'root', type: Types::STRING, nullable: true)] + #[Gedmo\TreeRoot] + private $root; +} diff --git a/lib/Gedmo/Tree/TreeListener.php b/src/Tree/TreeListener.php similarity index 50% rename from lib/Gedmo/Tree/TreeListener.php rename to src/Tree/TreeListener.php index c359329770..8c73a45898 100644 --- a/lib/Gedmo/Tree/TreeListener.php +++ b/src/Tree/TreeListener.php @@ -1,10 +1,26 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Tree; use Doctrine\Common\EventArgs; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Event\ManagerEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Exception\UnexpectedValueException; use Gedmo\Mapping\MappedEventSubscriber; -use Doctrine\Common\Persistence\ObjectManager; +use Gedmo\Tree\Mapping\Event\TreeAdapter; /** * The tree listener handles the synchronization of @@ -12,39 +28,69 @@ * strategies on handling the tree. * * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @phpstan-type TreeConfiguration = array{ + * activate_locking?: bool, + * closure?: class-string, + * left?: string, + * level?: string, + * lock_time?: string, + * locking_timeout?: int, + * parent?: string, + * path?: string, + * path_source?: string, + * path_separator?: string, + * path_append_id?: ?bool, + * path_starts_with_separator?: bool, + * path_ends_with_separator?: bool, + * path_hash?: string, + * right?: string, + * root?: string, + * rootIdentifierMethod?: string, + * strategy?: string, + * useObjectClass?: class-string, + * level_base?: int, + * } + * + * @phpstan-extends MappedEventSubscriber */ class TreeListener extends MappedEventSubscriber { /** * Tree processing strategies for object classes * - * @var array + * @var array + * + * @phpstan-var array */ - private $strategies = array(); + private array $strategies = []; /** * List of strategy instances * - * @var array + * @var array + * + * @phpstan-var array, Strategy> */ - private $strategyInstances = array(); + private array $strategyInstances = []; /** * List of used classes on flush * - * @var array + * @var array + * + * @phpstan-var array */ - private $usedClassesOnFlush = array(); + private array $usedClassesOnFlush = []; /** * Specifies the list of events to listen * - * @return array + * @return string[] */ public function getSubscribedEvents() { - return array( + return [ 'prePersist', 'preRemove', 'preUpdate', @@ -53,14 +99,13 @@ public function getSubscribedEvents() 'postPersist', 'postUpdate', 'postRemove', - ); + ]; } /** * Get the used strategy for tree processing * - * @param ObjectManager $om - * @param string $class + * @param string $class * * @return Strategy */ @@ -68,20 +113,20 @@ public function getStrategy(ObjectManager $om, $class) { if (!isset($this->strategies[$class])) { $config = $this->getConfiguration($om, $class); - if (!$config) { - throw new \Gedmo\Exception\UnexpectedValueException("Tree object class: {$class} must have tree metadata at this point"); + if ([] === $config) { + throw new UnexpectedValueException("Tree object class: {$class} must have tree metadata at this point"); } $managerName = 'UnsupportedManager'; - if ($om instanceof \Doctrine\ORM\EntityManager) { + if ($om instanceof EntityManagerInterface) { $managerName = 'ORM'; - } elseif ($om instanceof \Doctrine\ODM\MongoDB\DocumentManager) { + } elseif ($om instanceof DocumentManager) { $managerName = 'ODM\\MongoDB'; } if (!isset($this->strategyInstances[$config['strategy']])) { $strategyClass = $this->getNamespace().'\\Strategy\\'.$managerName.'\\'.ucfirst($config['strategy']); if (!class_exists($strategyClass)) { - throw new \Gedmo\Exception\InvalidArgumentException($managerName." TreeListener does not support tree type: {$config['strategy']}"); + throw new InvalidArgumentException($managerName." TreeListener does not support tree type: {$config['strategy']}"); } $this->strategyInstances[$config['strategy']] = new $strategyClass($this); } @@ -95,7 +140,11 @@ public function getStrategy(ObjectManager $om, $class) * Looks for Tree objects being updated * for further processing * - * @param EventArgs $args + * @param ManagerEventArgs $args + * + * @phpstan-param ManagerEventArgs $args + * + * @return void */ public function onFlush(EventArgs $args) { @@ -106,26 +155,26 @@ public function onFlush(EventArgs $args) // check all scheduled updates for TreeNodes foreach ($ea->getScheduledObjectInsertions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name)) { - $this->usedClassesOnFlush[$meta->name] = null; - $this->getStrategy($om, $meta->name)->processScheduledInsertion($om, $object, $ea); + if ($this->getConfiguration($om, $meta->getName())) { + $this->usedClassesOnFlush[$meta->getName()] = null; + $this->getStrategy($om, $meta->getName())->processScheduledInsertion($om, $object, $ea); $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); } } foreach ($ea->getScheduledObjectUpdates($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name)) { - $this->usedClassesOnFlush[$meta->name] = null; - $this->getStrategy($om, $meta->name)->processScheduledUpdate($om, $object, $ea); + if ($this->getConfiguration($om, $meta->getName())) { + $this->usedClassesOnFlush[$meta->getName()] = null; + $this->getStrategy($om, $meta->getName())->processScheduledUpdate($om, $object, $ea); } } foreach ($ea->getScheduledObjectDeletions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name)) { - $this->usedClassesOnFlush[$meta->name] = null; - $this->getStrategy($om, $meta->name)->processScheduledDelete($om, $object); + if ($this->getConfiguration($om, $meta->getName())) { + $this->usedClassesOnFlush[$meta->getName()] = null; + $this->getStrategy($om, $meta->getName())->processScheduledDelete($om, $object); } } @@ -137,7 +186,11 @@ public function onFlush(EventArgs $args) /** * Updates tree on Node removal * - * @param EventArgs $args + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void */ public function preRemove(EventArgs $args) { @@ -146,15 +199,19 @@ public function preRemove(EventArgs $args) $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name)) { - $this->getStrategy($om, $meta->name)->processPreRemove($om, $object); + if ($this->getConfiguration($om, $meta->getName())) { + $this->getStrategy($om, $meta->getName())->processPreRemove($om, $object); } } /** * Checks for persisted Nodes * - * @param EventArgs $args + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void */ public function prePersist(EventArgs $args) { @@ -163,15 +220,19 @@ public function prePersist(EventArgs $args) $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name)) { - $this->getStrategy($om, $meta->name)->processPrePersist($om, $object); + if ($this->getConfiguration($om, $meta->getName())) { + $this->getStrategy($om, $meta->getName())->processPrePersist($om, $object); } } /** * Checks for updated Nodes * - * @param EventArgs $args + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void */ public function preUpdate(EventArgs $args) { @@ -180,8 +241,8 @@ public function preUpdate(EventArgs $args) $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name)) { - $this->getStrategy($om, $meta->name)->processPreUpdate($om, $object); + if ($this->getConfiguration($om, $meta->getName())) { + $this->getStrategy($om, $meta->getName())->processPreUpdate($om, $object); } } @@ -189,7 +250,11 @@ public function preUpdate(EventArgs $args) * Checks for pending Nodes to fully synchronize * the tree * - * @param EventArgs $args + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void */ public function postPersist(EventArgs $args) { @@ -198,8 +263,8 @@ public function postPersist(EventArgs $args) $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name)) { - $this->getStrategy($om, $meta->name)->processPostPersist($om, $object, $ea); + if ($this->getConfiguration($om, $meta->getName())) { + $this->getStrategy($om, $meta->getName())->processPostPersist($om, $object, $ea); } } @@ -207,7 +272,11 @@ public function postPersist(EventArgs $args) * Checks for pending Nodes to fully synchronize * the tree * - * @param EventArgs $args + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void */ public function postUpdate(EventArgs $args) { @@ -216,8 +285,8 @@ public function postUpdate(EventArgs $args) $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name)) { - $this->getStrategy($om, $meta->name)->processPostUpdate($om, $object, $ea); + if ($this->getConfiguration($om, $meta->getName())) { + $this->getStrategy($om, $meta->getName())->processPostUpdate($om, $object, $ea); } } @@ -225,7 +294,11 @@ public function postUpdate(EventArgs $args) * Checks for pending Nodes to fully synchronize * the tree * - * @param EventArgs $args + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args + * + * @return void */ public function postRemove(EventArgs $args) { @@ -234,30 +307,30 @@ public function postRemove(EventArgs $args) $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); - if ($this->getConfiguration($om, $meta->name)) { - $this->getStrategy($om, $meta->name)->processPostRemove($om, $object, $ea); + if ($this->getConfiguration($om, $meta->getName())) { + $this->getStrategy($om, $meta->getName())->processPostRemove($om, $object, $ea); } } /** - * Mapps additional metadata + * Maps additional metadata * - * @param EventArgs $eventArgs + * @param LoadClassMetadataEventArgs $eventArgs + * + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $eventArgs + * + * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { - $ea = $this->getEventAdapter($eventArgs); - $om = $ea->getObjectManager(); + $om = $eventArgs->getObjectManager(); $meta = $eventArgs->getClassMetadata(); $this->loadMetadataForObjectClass($om, $meta); - if (isset(self::$configurations[$this->name][$meta->name]) && self::$configurations[$this->name][$meta->name]) { - $this->getStrategy($om, $meta->name)->processMetadataLoad($om, $meta); + if (isset(self::$configurations[$this->name][$meta->getName()]) && self::$configurations[$this->name][$meta->getName()]) { + $this->getStrategy($om, $meta->getName())->processMetadataLoad($om, $meta); } } - /** - * {@inheritDoc} - */ protected function getNamespace() { return __NAMESPACE__; @@ -267,13 +340,15 @@ protected function getNamespace() * Get the list of strategy instances used for * given object classes * - * @param array $classes + * @phpstan-param array $classes + * + * @return array * - * @return Strategy[] + * @phpstan-return array, Strategy> */ protected function getStrategiesUsedForObjects(array $classes) { - $strategies = array(); + $strategies = []; foreach ($classes as $name => $opt) { if (isset($this->strategies[$name]) && !isset($strategies[$this->strategies[$name]])) { $strategies[$this->strategies[$name]] = $this->strategyInstances[$this->strategies[$name]]; diff --git a/lib/Gedmo/Uploadable/Event/UploadableBaseEventArgs.php b/src/Uploadable/Event/UploadableBaseEventArgs.php similarity index 50% rename from lib/Gedmo/Uploadable/Event/UploadableBaseEventArgs.php rename to src/Uploadable/Event/UploadableBaseEventArgs.php index d06c423a25..4f96b7b166 100644 --- a/lib/Gedmo/Uploadable/Event/UploadableBaseEventArgs.php +++ b/src/Uploadable/Event/UploadableBaseEventArgs.php @@ -1,9 +1,18 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\Event; use Doctrine\Common\EventArgs; -use Doctrine\ORM\EntityManager; +use Doctrine\Deprecations\Deprecation; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ObjectManager; use Gedmo\Uploadable\FileInfo\FileInfoInterface; use Gedmo\Uploadable\UploadableListener; @@ -12,57 +21,52 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ - abstract class UploadableBaseEventArgs extends EventArgs { /** * The instance of the Uploadable listener that fired this event - * - * @var \Gedmo\Uploadable\UploadableListener */ - private $uploadableListener; + private UploadableListener $uploadableListener; + + private EntityManagerInterface $em; /** - * @var \Doctrine\ORM\EntityManager + * @todo Check if this property must be removed, as it is not used. */ - private $em; + private array $config = []; /** * The Uploadable entity * - * @var object $entity + * @var object */ private $entity; /** * The configuration of the Uploadable extension for this entity class * - * @var array $extensionConfiguration + * @todo Check if this property must be removed, as it is never set. + * + * @var array */ private $extensionConfiguration; - /** - * @var \Gedmo\Uploadable\FileInfo\FileInfoInterface - */ - private $fileInfo; + private FileInfoInterface $fileInfo; /** - * @var string $action - Is the file being created, updated or removed? - * This value can be: CREATE, UPDATE or DELETE. + * Is the file being created, updated or removed? + * This value can be: CREATE, UPDATE or DELETE + * + * @var string */ private $action; /** - * @param UploadableListener $listener - * @param \Doctrine\ORM\EntityManager $em - * @param array $config - * @param FileInfoInterface $fileInfo - * @param object $entity - * @param string $action + * @param object $entity + * @param string $action */ - public function __construct(UploadableListener $listener, EntityManager $em, array $config, FileInfoInterface $fileInfo, $entity, $action) + public function __construct(UploadableListener $listener, EntityManagerInterface $em, array $config, FileInfoInterface $fileInfo, $entity, $action) { $this->uploadableListener = $listener; $this->em = $em; @@ -75,7 +79,7 @@ public function __construct(UploadableListener $listener, EntityManager $em, arr /** * Retrieve the associated listener * - * @return \Gedmo\Uploadable\UploadableListener + * @return UploadableListener */ public function getListener() { @@ -85,9 +89,26 @@ public function getListener() /** * Retrieve associated EntityManager * - * @return \Doctrine\ORM\EntityManager + * @return EntityManagerInterface */ public function getEntityManager() + { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2639', + '"%s()" is deprecated since gedmo/doctrine-extensions 3.14 and will be removed in version 4.0.', + __METHOD__ + ); + + return $this->em; + } + + /** + * Retrieve associated EntityManager + * + * @return ObjectManager + */ + public function getObjectManager() { return $this->em; } @@ -98,6 +119,23 @@ public function getEntityManager() * @return object */ public function getEntity() + { + Deprecation::trigger( + 'gedmo/doctrine-extensions', + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2639', + '"%s()" is deprecated since gedmo/doctrine-extensions 3.14 and will be removed in version 4.0.', + __METHOD__ + ); + + return $this->entity; + } + + /** + * Retrieve associated Object + * + * @return object + */ + public function getObject() { return $this->entity; } @@ -115,7 +153,7 @@ public function getExtensionConfiguration() /** * Retrieve the FileInfo associated with this entity. * - * @return \Gedmo\Uploadable\FileInfo\FileInfoInterface + * @return FileInfoInterface */ public function getFileInfo() { diff --git a/src/Uploadable/Event/UploadablePostFileProcessEventArgs.php b/src/Uploadable/Event/UploadablePostFileProcessEventArgs.php new file mode 100644 index 0000000000..5835f6aa47 --- /dev/null +++ b/src/Uploadable/Event/UploadablePostFileProcessEventArgs.php @@ -0,0 +1,22 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Uploadable\Event; + +/** + * Post File Process Event for the Uploadable extension + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadablePostFileProcessEventArgs extends UploadableBaseEventArgs +{ +} diff --git a/src/Uploadable/Event/UploadablePreFileProcessEventArgs.php b/src/Uploadable/Event/UploadablePreFileProcessEventArgs.php new file mode 100644 index 0000000000..465c099dde --- /dev/null +++ b/src/Uploadable/Event/UploadablePreFileProcessEventArgs.php @@ -0,0 +1,22 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Uploadable\Event; + +/** + * Pre File Process Event for the Uploadable extension + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @final since gedmo/doctrine-extensions 3.11 + */ +class UploadablePreFileProcessEventArgs extends UploadableBaseEventArgs +{ +} diff --git a/lib/Gedmo/Uploadable/Events.php b/src/Uploadable/Events.php similarity index 64% rename from lib/Gedmo/Uploadable/Events.php rename to src/Uploadable/Events.php index 8876ddfd58..7b81d1caae 100644 --- a/lib/Gedmo/Uploadable/Events.php +++ b/src/Uploadable/Events.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable; /** @@ -7,14 +14,9 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ - final class Events { - private function __construct() - { - } /** * The uploadablePreFileProcess event occurs before a file is processed inside * the Uploadable listener. This means it happens before the file is validated and moved @@ -22,7 +24,7 @@ private function __construct() * * @var string */ - const uploadablePreFileProcess = 'uploadablePreFileProcess'; + public const uploadablePreFileProcess = 'uploadablePreFileProcess'; /** * The uploadablePostFileProcess event occurs after a file is processed inside * the Uploadable listener. This means it happens after the file is validated and moved @@ -30,5 +32,9 @@ private function __construct() * * @var string */ - const uploadablePostFileProcess = 'uploadablePostFileProcess'; + public const uploadablePostFileProcess = 'uploadablePostFileProcess'; + + private function __construct() + { + } } diff --git a/lib/Gedmo/Uploadable/FileInfo/FileInfoArray.php b/src/Uploadable/FileInfo/FileInfoArray.php similarity index 64% rename from lib/Gedmo/Uploadable/FileInfo/FileInfoArray.php rename to src/Uploadable/FileInfo/FileInfoArray.php index 384f0e8eb3..32e822f9cc 100644 --- a/lib/Gedmo/Uploadable/FileInfo/FileInfoArray.php +++ b/src/Uploadable/FileInfo/FileInfoArray.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\FileInfo; /** @@ -7,16 +14,24 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ - class FileInfoArray implements FileInfoInterface { + /** + * @var array + * + * @phpstan-var array{error: int, size: int, type: string, tmp_name: string, name: string} + */ protected $fileInfo; + /** + * @param array $fileInfo + */ public function __construct(array $fileInfo) { - $keys = array('error', 'size', 'type', 'tmp_name', 'name'); + $keys = ['error', 'size', 'type', 'tmp_name', 'name']; foreach ($keys as $k) { if (!isset($fileInfo[$k])) { diff --git a/lib/Gedmo/Uploadable/FileInfo/FileInfoInterface.php b/src/Uploadable/FileInfo/FileInfoInterface.php similarity index 53% rename from lib/Gedmo/Uploadable/FileInfo/FileInfoInterface.php rename to src/Uploadable/FileInfo/FileInfoInterface.php index f0b7da7f6f..a051235472 100644 --- a/lib/Gedmo/Uploadable/FileInfo/FileInfoInterface.php +++ b/src/Uploadable/FileInfo/FileInfoInterface.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\FileInfo; /** @@ -7,15 +14,32 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ - interface FileInfoInterface { + /** + * @return string|null + */ public function getTmpName(); + + /** + * @return string|null + */ public function getName(); + + /** + * @return int|null + */ public function getSize(); + + /** + * @return string|null + */ public function getType(); + + /** + * @return int + */ public function getError(); /** diff --git a/lib/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumeric.php b/src/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumeric.php similarity index 62% rename from lib/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumeric.php rename to src/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumeric.php index 88c27c179b..966e03ab79 100644 --- a/lib/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumeric.php +++ b/src/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumeric.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\FilenameGenerator; /** @@ -10,14 +17,11 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ - class FilenameGeneratorAlphanumeric implements FilenameGeneratorInterface { - /** - * @inheritDoc - */ public static function generate($filename, $extension, $object = null) { return preg_replace('/[^a-z0-9]+/', '-', strtolower($filename)).$extension; diff --git a/src/Uploadable/FilenameGenerator/FilenameGeneratorInterface.php b/src/Uploadable/FilenameGenerator/FilenameGeneratorInterface.php new file mode 100644 index 0000000000..663ce0a57b --- /dev/null +++ b/src/Uploadable/FilenameGenerator/FilenameGeneratorInterface.php @@ -0,0 +1,30 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Uploadable\FilenameGenerator; + +/** + * FilenameGeneratorInterface + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +interface FilenameGeneratorInterface +{ + /** + * Generates a new filename + * + * @param string $filename Filename without extension + * @param string $extension Extension with dot: .jpg, .gif, etc + * @param object|null $object + * + * @return string + */ + public static function generate($filename, $extension, $object = null); +} diff --git a/lib/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorSha1.php b/src/Uploadable/FilenameGenerator/FilenameGeneratorSha1.php similarity index 56% rename from lib/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorSha1.php rename to src/Uploadable/FilenameGenerator/FilenameGeneratorSha1.php index 3dcdeb4f4f..8f95abb803 100644 --- a/lib/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorSha1.php +++ b/src/Uploadable/FilenameGenerator/FilenameGeneratorSha1.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\FilenameGenerator; /** @@ -7,14 +14,11 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ - class FilenameGeneratorSha1 implements FilenameGeneratorInterface { - /** - * @inheritDoc - */ public static function generate($filename, $extension, $object = null) { return sha1(uniqid($filename.$extension, true)).$extension; diff --git a/src/Uploadable/Mapping/Driver/Annotation.php b/src/Uploadable/Mapping/Driver/Annotation.php new file mode 100644 index 0000000000..2d9172a38b --- /dev/null +++ b/src/Uploadable/Mapping/Driver/Annotation.php @@ -0,0 +1,26 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Uploadable\Mapping\Driver; + +use Gedmo\Mapping\Driver\AnnotationDriverInterface; + +/** + * Mapping driver for the uploaded extension which reads extended metadata from annotations on an uploadable class. + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. + * + * @internal + */ +class Annotation extends Attribute implements AnnotationDriverInterface +{ +} diff --git a/lib/Gedmo/Uploadable/Mapping/Driver/Annotation.php b/src/Uploadable/Mapping/Driver/Attribute.php similarity index 64% rename from lib/Gedmo/Uploadable/Mapping/Driver/Annotation.php rename to src/Uploadable/Mapping/Driver/Attribute.php index 6405debec5..e8f12f87e0 100644 --- a/lib/Gedmo/Uploadable/Mapping/Driver/Annotation.php +++ b/src/Uploadable/Mapping/Driver/Attribute.php @@ -1,40 +1,65 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\Mapping\Driver; +use Gedmo\Mapping\Annotation\Uploadable; +use Gedmo\Mapping\Annotation\UploadableFileMimeType; +use Gedmo\Mapping\Annotation\UploadableFileName; +use Gedmo\Mapping\Annotation\UploadableFilePath; +use Gedmo\Mapping\Annotation\UploadableFileSize; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; use Gedmo\Uploadable\Mapping\Validator; /** - * This is an annotation mapping driver for Uploadable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Uploadable - * extension. + * Mapping driver for the uploaded extension which reads extended metadata from attributes on an uploadable class. * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ -class Annotation extends AbstractAnnotationDriver +class Attribute extends AbstractAnnotationDriver { /** - * Annotation to define that this object is loggable + * Mapping object for the uploadable extension. + */ + public const UPLOADABLE = Uploadable::class; + + /** + * Mapping object to mark the field which will store the MIME type for an upload. + */ + public const UPLOADABLE_FILE_MIME_TYPE = UploadableFileMimeType::class; + + /** + * Mapping object to mark the field which will store the file name for an upload. + */ + public const UPLOADABLE_FILE_NAME = UploadableFileName::class; + + /** + * Mapping object to mark the field which will store the filesystem path for an upload. */ - const UPLOADABLE = 'Gedmo\\Mapping\\Annotation\\Uploadable'; - const UPLOADABLE_FILE_MIME_TYPE = 'Gedmo\\Mapping\\Annotation\\UploadableFileMimeType'; - const UPLOADABLE_FILE_NAME = 'Gedmo\\Mapping\\Annotation\\UploadableFileName'; - const UPLOADABLE_FILE_PATH = 'Gedmo\\Mapping\\Annotation\\UploadableFilePath'; - const UPLOADABLE_FILE_SIZE = 'Gedmo\\Mapping\\Annotation\\UploadableFileSize'; + public const UPLOADABLE_FILE_PATH = UploadableFilePath::class; /** - * {@inheritDoc} + * Mapping object to mark the field which will store the file size for an upload. */ + public const UPLOADABLE_FILE_SIZE = UploadableFileSize::class; + public function readExtendedMetadata($meta, array &$config) { $class = $this->getMetaReflectionClass($meta); // class annotations if ($annot = $this->reader->getClassAnnotation($class, self::UPLOADABLE)) { + \assert($annot instanceof Uploadable); + $config['uploadable'] = true; $config['allowOverwrite'] = $annot->allowOverwrite; $config['appendNumber'] = $annot->appendNumber; @@ -46,7 +71,7 @@ public function readExtendedMetadata($meta, array &$config) $config['fileSizeField'] = false; $config['callback'] = $annot->callback; $config['filenameGenerator'] = $annot->filenameGenerator; - $config['maxSize'] = (double) $annot->maxSize; + $config['maxSize'] = (float) $annot->maxSize; $config['allowedTypes'] = $annot->allowedTypes; $config['disallowedTypes'] = $annot->disallowedTypes; @@ -68,7 +93,7 @@ public function readExtendedMetadata($meta, array &$config) } } - Validator::validateConfiguration($meta, $config); + $config = Validator::validateConfiguration($meta, $config); } /* @@ -83,6 +108,8 @@ public function readExtendedMetadata($meta, array &$config) $refl = new \ReflectionClass($association['targetEntity']); if ($annot = $this->reader->getClassAnnotation($refl, self::UPLOADABLE)) { + \assert($annot instanceof Uploadable); + $config['hasUploadables'] = true; if (!isset($config['uploadables'])) { @@ -98,5 +125,7 @@ public function readExtendedMetadata($meta, array &$config) }*/ $this->validateFullMetadata($meta, $config); + + return $config; } } diff --git a/lib/Gedmo/Uploadable/Mapping/Driver/Xml.php b/src/Uploadable/Mapping/Driver/Xml.php similarity index 84% rename from lib/Gedmo/Uploadable/Mapping/Driver/Xml.php rename to src/Uploadable/Mapping/Driver/Xml.php index c52e6f1b23..c8b9316276 100644 --- a/lib/Gedmo/Uploadable/Mapping/Driver/Xml.php +++ b/src/Uploadable/Mapping/Driver/Xml.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\Mapping\Driver; use Gedmo\Mapping\Driver\Xml as BaseXml; @@ -14,23 +21,21 @@ * @author Gustavo Falco * @author Gediminas Morkevicius * @author Miha Vrhovnik - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @internal */ class Xml extends BaseXml { - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { /** - * @var \SimpleXmlElement $xml + * @var \SimpleXmlElement */ - $xml = $this->_getMapping($meta->name); + $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); - if ($xmlDoctrine->getName() == 'entity' || $xmlDoctrine->getName() == 'mapped-superclass') { + if (in_array($xmlDoctrine->getName(), ['mapped-superclass', 'entity'], true)) { if (isset($xml->uploadable)) { $xmlUploadable = $xml->uploadable; $config['uploadable'] = true; @@ -52,8 +57,8 @@ public function readExtendedMetadata($meta, array &$config) $this->_getAttribute($xml->{'uploadable'}, 'filename-generator') : Validator::FILENAME_GENERATOR_NONE; $config['maxSize'] = $this->_isAttributeSet($xmlUploadable, 'max-size') ? - (double) $this->_getAttribute($xml->{'uploadable'}, 'max-size') : - (double) 0; + (float) $this->_getAttribute($xml->{'uploadable'}, 'max-size') : + (float) 0; $config['allowedTypes'] = $this->_isAttributeSet($xmlUploadable, 'allowed-types') ? $this->_getAttribute($xml->{'uploadable'}, 'allowed-types') : ''; @@ -80,8 +85,10 @@ public function readExtendedMetadata($meta, array &$config) } } - Validator::validateConfiguration($meta, $config); + $config = Validator::validateConfiguration($meta, $config); } } + + return $config; } } diff --git a/lib/Gedmo/Uploadable/Mapping/Driver/Yaml.php b/src/Uploadable/Mapping/Driver/Yaml.php similarity index 59% rename from lib/Gedmo/Uploadable/Mapping/Driver/Yaml.php rename to src/Uploadable/Mapping/Driver/Yaml.php index ca186e402b..540389aa92 100644 --- a/lib/Gedmo/Uploadable/Mapping/Driver/Yaml.php +++ b/src/Uploadable/Mapping/Driver/Yaml.php @@ -1,9 +1,16 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\Mapping\Driver; -use Gedmo\Mapping\Driver\File; use Gedmo\Mapping\Driver; +use Gedmo\Mapping\Driver\File; use Gedmo\Uploadable\Mapping\Validator; /** @@ -14,22 +21,23 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. + * + * @internal */ class Yaml extends File implements Driver { /** * File extension + * * @var string */ protected $_extension = '.dcm.yml'; - /** - * {@inheritDoc} - */ public function readExtendedMetadata($meta, array &$config) { - $mapping = $this->_getMapping($meta->name); + $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['gedmo'])) { $classMapping = $mapping['gedmo']; @@ -42,50 +50,43 @@ public function readExtendedMetadata($meta, array &$config) (bool) $uploadable['allowOverwrite'] : false; $config['appendNumber'] = isset($uploadable['appendNumber']) ? (bool) $uploadable['appendNumber'] : false; - $config['path'] = isset($uploadable['path']) ? $uploadable['path'] : ''; - $config['pathMethod'] = isset($uploadable['pathMethod']) ? $uploadable['pathMethod'] : ''; - $config['callback'] = isset($uploadable['callback']) ? $uploadable['callback'] : ''; + $config['path'] = $uploadable['path'] ?? ''; + $config['pathMethod'] = $uploadable['pathMethod'] ?? ''; + $config['callback'] = $uploadable['callback'] ?? ''; $config['fileMimeTypeField'] = false; $config['fileNameField'] = false; $config['filePathField'] = false; $config['fileSizeField'] = false; - $config['filenameGenerator'] = isset($uploadable['filenameGenerator']) ? - $uploadable['filenameGenerator'] : - Validator::FILENAME_GENERATOR_NONE; + $config['filenameGenerator'] = $uploadable['filenameGenerator'] ?? Validator::FILENAME_GENERATOR_NONE; $config['maxSize'] = isset($uploadable['maxSize']) ? - (double) $uploadable['maxSize'] : - (double) 0; - $config['allowedTypes'] = isset($uploadable['allowedTypes']) ? - $uploadable['allowedTypes'] : - ''; - $config['disallowedTypes'] = isset($uploadable['disallowedTypes']) ? - $uploadable['disallowedTypes'] : - ''; + (float) $uploadable['maxSize'] : + (float) 0; + $config['allowedTypes'] = $uploadable['allowedTypes'] ?? ''; + $config['disallowedTypes'] = $uploadable['disallowedTypes'] ?? ''; if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $info) { if (isset($info['gedmo']) && array_key_exists(0, $info['gedmo'])) { - if ($info['gedmo'][0] === 'uploadableFileMimeType') { + if ('uploadableFileMimeType' === $info['gedmo'][0]) { $config['fileMimeTypeField'] = $field; - } elseif ($info['gedmo'][0] === 'uploadableFileSize') { + } elseif ('uploadableFileSize' === $info['gedmo'][0]) { $config['fileSizeField'] = $field; - } elseif ($info['gedmo'][0] === 'uploadableFileName') { + } elseif ('uploadableFileName' === $info['gedmo'][0]) { $config['fileNameField'] = $field; - } elseif ($info['gedmo'][0] === 'uploadableFilePath') { + } elseif ('uploadableFilePath' === $info['gedmo'][0]) { $config['filePathField'] = $field; } } } } - Validator::validateConfiguration($meta, $config); + $config = Validator::validateConfiguration($meta, $config); } } + + return $config; } - /** - * {@inheritDoc} - */ protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); diff --git a/lib/Gedmo/Uploadable/Mapping/Validator.php b/src/Uploadable/Mapping/Validator.php similarity index 53% rename from lib/Gedmo/Uploadable/Mapping/Validator.php rename to src/Uploadable/Mapping/Validator.php index 60bdffa340..d158fb469b 100644 --- a/lib/Gedmo/Uploadable/Mapping/Validator.php +++ b/src/Uploadable/Mapping/Validator.php @@ -1,29 +1,37 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\Mapping; +use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Exception\UploadableCantWriteException; use Gedmo\Exception\UploadableInvalidPathException; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorInterface; /** * This class is used to validate mapping information * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ - class Validator { - const UPLOADABLE_FILE_MIME_TYPE = 'UploadableFileMimeType'; - const UPLOADABLE_FILE_NAME = 'UploadableFileName'; - const UPLOADABLE_FILE_PATH = 'UploadableFilePath'; - const UPLOADABLE_FILE_SIZE = 'UploadableFileSize'; - const FILENAME_GENERATOR_SHA1 = 'SHA1'; - const FILENAME_GENERATOR_ALPHANUMERIC = 'ALPHANUMERIC'; - const FILENAME_GENERATOR_NONE = 'NONE'; + public const UPLOADABLE_FILE_MIME_TYPE = 'UploadableFileMimeType'; + public const UPLOADABLE_FILE_NAME = 'UploadableFileName'; + public const UPLOADABLE_FILE_PATH = 'UploadableFilePath'; + public const UPLOADABLE_FILE_SIZE = 'UploadableFileSize'; + public const FILENAME_GENERATOR_SHA1 = 'SHA1'; + public const FILENAME_GENERATOR_ALPHANUMERIC = 'ALPHANUMERIC'; + public const FILENAME_GENERATOR_NONE = 'NONE'; /** * Determines if we should throw an exception in the case the "allowedTypes" and @@ -36,47 +44,47 @@ class Validator /** * List of types which are valid for UploadableFileMimeType field * - * @var array + * @var string[] */ - public static $validFileMimeTypeTypes = array( + public static $validFileMimeTypeTypes = [ 'string', - ); + ]; /** * List of types which are valid for UploadableFileName field * - * @var array + * @var string[] */ - public static $validFileNameTypes = array( + public static $validFileNameTypes = [ 'string', - ); + ]; /** * List of types which are valid for UploadableFilePath field * - * @var array + * @var string[] */ - public static $validFilePathTypes = array( + public static $validFilePathTypes = [ 'string', - ); + ]; /** * List of types which are valid for UploadableFileSize field for ORM * - * @var array + * @var string[] */ - public static $validFileSizeTypes = array( + public static $validFileSizeTypes = [ 'decimal', - ); + ]; /** * List of types which are valid for UploadableFileSize field for ODM * - * @var array + * @var string[] */ - public static $validFileSizeTypesODM = array( + public static $validFileSizeTypesODM = [ 'float', - ); + ]; /** * Whether to validate if the directory of the file exists and is writable, useful to disable it when using @@ -86,30 +94,58 @@ class Validator */ public static $validateWritableDirectory = true; + /** + * @param ClassMetadata $meta + * @param string $field + * + * @return void + */ public static function validateFileNameField(ClassMetadata $meta, $field) { self::validateField($meta, $field, self::UPLOADABLE_FILE_NAME, self::$validFileNameTypes); } + /** + * @param ClassMetadata $meta + * @param string $field + * + * @return void + */ public static function validateFileMimeTypeField(ClassMetadata $meta, $field) { self::validateField($meta, $field, self::UPLOADABLE_FILE_MIME_TYPE, self::$validFileMimeTypeTypes); } + /** + * @param ClassMetadata $meta + * @param string $field + * + * @return void + */ public static function validateFilePathField(ClassMetadata $meta, $field) { self::validateField($meta, $field, self::UPLOADABLE_FILE_PATH, self::$validFilePathTypes); } + /** + * @param ClassMetadata $meta + * @param string $field + * + * @return void + */ public static function validateFileSizeField(ClassMetadata $meta, $field) { - if ($meta instanceof \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo) { - self::validateField($meta, $field, self::UPLOADABLE_FILE_SIZE, self::$validFileSizeTypesODM); - } else { - self::validateField($meta, $field, self::UPLOADABLE_FILE_SIZE, self::$validFileSizeTypes); - } + self::validateField($meta, $field, self::UPLOADABLE_FILE_SIZE, self::$validFileSizeTypes); } + /** + * @param ClassMetadata $meta + * @param string $field + * @param string $uploadableField + * @param string[] $validFieldTypes + * + * @return void + */ public static function validateField($meta, $field, $uploadableField, $validFieldTypes) { if ($meta->isMappedSuperclass) { @@ -118,20 +154,21 @@ public static function validateField($meta, $field, $uploadableField, $validFiel $fieldMapping = $meta->getFieldMapping($field); - if (!in_array($fieldMapping['type'], $validFieldTypes)) { + if (!in_array($fieldMapping->type ?? $fieldMapping['type'], $validFieldTypes, true)) { $msg = 'Field "%s" to work as an "%s" field must be of one of the following types: "%s".'; - throw new InvalidMappingException(sprintf($msg, - $field, - $uploadableField, - implode(', ', $validFieldTypes) - )); + throw new InvalidMappingException(sprintf($msg, $field, $uploadableField, implode(', ', $validFieldTypes))); } } + /** + * @param string $path + * + * @return void + */ public static function validatePath($path) { - if (!is_string($path) || $path === '') { + if (!is_string($path) || '' === $path) { throw new UploadableInvalidPathException('Path must be a string containing the path to a valid directory.'); } @@ -140,62 +177,54 @@ public static function validatePath($path) } if (!is_dir($path) && !@mkdir($path, 0777, true)) { - throw new UploadableInvalidPathException(sprintf('Unable to create "%s" directory.', - $path - )); + throw new UploadableInvalidPathException(sprintf('Unable to create "%s" directory.', $path)); } if (!is_writable($path)) { - throw new UploadableCantWriteException(sprintf('Directory "%s" does is not writable.', - $path - )); + throw new UploadableCantWriteException(sprintf('Directory "%s" is not writable.', $path)); } } + /** + * @param ClassMetadata $meta + * @param array $config + * + * @return array + * + * @todo Stop receiving by reference the `$config` parameter and use `array` as return type declaration + */ public static function validateConfiguration(ClassMetadata $meta, array &$config) { if (!$config['filePathField'] && !$config['fileNameField']) { - throw new InvalidMappingException(sprintf('Class "%s" must have an UploadableFilePath or UploadableFileName field.', - $meta->name - )); + throw new InvalidMappingException(sprintf('Class "%s" must have an UploadableFilePath or UploadableFileName field.', $meta->getName())); } $refl = $meta->getReflectionClass(); - if ($config['pathMethod'] !== '' && !$refl->hasMethod($config['pathMethod'])) { - throw new InvalidMappingException(sprintf('Class "%s" doesn\'t have method "%s"!', - $meta->name, - $config['pathMethod'] - )); + if ('' !== $config['pathMethod'] && !$refl->hasMethod($config['pathMethod'])) { + throw new InvalidMappingException(sprintf('Class "%s" doesn\'t have method "%s"!', $meta->getName(), $config['pathMethod'])); } - if ($config['callback'] !== '' && !$refl->hasMethod($config['callback'])) { - throw new InvalidMappingException(sprintf('Class "%s" doesn\'t have method "%s"!', - $meta->name, - $config['callback'] - )); + if ('' !== $config['callback'] && !$refl->hasMethod($config['callback'])) { + throw new InvalidMappingException(sprintf('Class "%s" doesn\'t have method "%s"!', $meta->getName(), $config['callback'])); } - $config['maxSize'] = (double) $config['maxSize']; + $config['maxSize'] = (float) $config['maxSize']; if ($config['maxSize'] < 0) { - throw new InvalidMappingException(sprintf('Option "maxSize" must be a number >= 0 for class "%s".', - $meta->name - )); + throw new InvalidMappingException(sprintf('Option "maxSize" must be a number >= 0 for class "%s".', $meta->getName())); } - if (self::$enableMimeTypesConfigException && ($config['allowedTypes'] !== '' && $config['disallowedTypes'] !== '')) { + if (self::$enableMimeTypesConfigException && '' !== $config['allowedTypes'] && '' !== $config['disallowedTypes']) { $msg = 'You\'ve set "allowedTypes" and "disallowedTypes" options. You must set only one in class "%s".'; - throw new InvalidMappingException(sprintf($msg, - $meta->name - )); + throw new InvalidMappingException(sprintf($msg, $meta->getName())); } - $config['allowedTypes'] = $config['allowedTypes'] ? (strpos($config['allowedTypes'], ',') !== false ? - explode(',', $config['allowedTypes']) : array($config['allowedTypes'])) : false; - $config['disallowedTypes'] = $config['disallowedTypes'] ? (strpos($config['disallowedTypes'], ',') !== false ? - explode(',', $config['disallowedTypes']) : array($config['disallowedTypes'])) : false; + $config['allowedTypes'] = $config['allowedTypes'] ? (false !== strpos($config['allowedTypes'], ',') ? + explode(',', $config['allowedTypes']) : [$config['allowedTypes']]) : false; + $config['disallowedTypes'] = $config['disallowedTypes'] ? (false !== strpos($config['disallowedTypes'], ',') ? + explode(',', $config['disallowedTypes']) : [$config['disallowedTypes']]) : false; if ($config['fileNameField']) { self::validateFileNameField($meta, $config['fileNameField']); @@ -219,24 +248,11 @@ public static function validateConfiguration(ClassMetadata $meta, array &$config case self::FILENAME_GENERATOR_NONE: break; default: - $ok = false; - - if (class_exists($config['filenameGenerator'])) { - $refl = new \ReflectionClass($config['filenameGenerator']); - - if ($refl->implementsInterface('Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorInterface')) { - $ok = true; - } - } - - if (!$ok) { - $msg = 'Class "%s" needs a valid value for filenameGenerator. It can be: SHA1, ALPHANUMERIC, NONE or '; - $msg .= 'a class implementing FileGeneratorInterface.'; - - throw new InvalidMappingException(sprintf($msg, - $meta->name - )); + if (!class_exists($config['filenameGenerator']) || !is_subclass_of($config['filenameGenerator'], FilenameGeneratorInterface::class)) { + throw new InvalidMappingException(sprintf('Class "%s" needs a valid value for filenameGenerator. It can be: SHA1, ALPHANUMERIC, NONE or a class implementing %s.', $meta->getName(), FilenameGeneratorInterface::class)); } } + + return $config; } } diff --git a/lib/Gedmo/Uploadable/MimeType/MimeTypeGuesser.php b/src/Uploadable/MimeType/MimeTypeGuesser.php similarity index 67% rename from lib/Gedmo/Uploadable/MimeType/MimeTypeGuesser.php rename to src/Uploadable/MimeType/MimeTypeGuesser.php index 5bce8c5eda..9b3fcee432 100644 --- a/lib/Gedmo/Uploadable/MimeType/MimeTypeGuesser.php +++ b/src/Uploadable/MimeType/MimeTypeGuesser.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\MimeType; use Gedmo\Exception\UploadableFileNotReadableException; @@ -10,22 +17,19 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @final since gedmo/doctrine-extensions 3.11 */ class MimeTypeGuesser implements MimeTypeGuesserInterface { public function guess($filePath) { if (!is_file($filePath)) { - throw new UploadableInvalidFileException(sprintf('File "%s" does not exist.', - $filePath - )); + throw new UploadableInvalidFileException(sprintf('File "%s" does not exist.', $filePath)); } if (!is_readable($filePath)) { - throw new UploadableFileNotReadableException(sprintf('File "%s" is not readable.', - $filePath - )); + throw new UploadableFileNotReadableException(sprintf('File "%s" is not readable.', $filePath)); } if (function_exists('finfo_open')) { diff --git a/src/Uploadable/MimeType/MimeTypeGuesserInterface.php b/src/Uploadable/MimeType/MimeTypeGuesserInterface.php new file mode 100644 index 0000000000..ccd24a6637 --- /dev/null +++ b/src/Uploadable/MimeType/MimeTypeGuesserInterface.php @@ -0,0 +1,26 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Uploadable\MimeType; + +/** + * Interface for mime type guessers + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +interface MimeTypeGuesserInterface +{ + /** + * @param string $filePath + * + * @return string|null + */ + public function guess($filePath); +} diff --git a/lib/Gedmo/Uploadable/MimeType/MimeTypesExtensionsMap.php b/src/Uploadable/MimeType/MimeTypesExtensionsMap.php similarity index 98% rename from lib/Gedmo/Uploadable/MimeType/MimeTypesExtensionsMap.php rename to src/Uploadable/MimeType/MimeTypesExtensionsMap.php index 7a6f2a448b..0aabfbec2c 100644 --- a/lib/Gedmo/Uploadable/MimeType/MimeTypesExtensionsMap.php +++ b/src/Uploadable/MimeType/MimeTypesExtensionsMap.php @@ -1,5 +1,12 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable\MimeType; /** @@ -7,16 +14,15 @@ * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ abstract class MimeTypesExtensionsMap { /** * Map of mime types and their default extensions. * - * @var array + * @var array */ - public static $map = array( + public static $map = [ 'application/andrew-inset' => 'ez', 'application/applixware' => 'aw', 'application/atom+xml' => 'atom', @@ -718,5 +724,5 @@ abstract class MimeTypesExtensionsMap 'video/x-msvideo' => 'avi', 'video/x-sgi-movie' => 'movie', 'x-conference/x-cooltalk' => 'ice', - ); + ]; } diff --git a/src/Uploadable/Uploadable.php b/src/Uploadable/Uploadable.php new file mode 100644 index 0000000000..e29badea40 --- /dev/null +++ b/src/Uploadable/Uploadable.php @@ -0,0 +1,31 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Uploadable; + +/** + * Marker interface for objects which can be identified as uploadable. + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +interface Uploadable +{ + // this interface is not necessary to implement + + /* + * @Gedmo\Uploadable + * to mark the class as Uploadable use class annotation @Gedmo\Uploadable + * this object will be able Uploadable + * example: + * + * @Gedmo\Uploadable + * class MyEntity + */ +} diff --git a/lib/Gedmo/Uploadable/UploadableListener.php b/src/Uploadable/UploadableListener.php similarity index 65% rename from lib/Gedmo/Uploadable/UploadableListener.php rename to src/Uploadable/UploadableListener.php index 4417057345..95efcf0cf1 100644 --- a/lib/Gedmo/Uploadable/UploadableListener.php +++ b/src/Uploadable/UploadableListener.php @@ -1,43 +1,75 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Gedmo\Uploadable; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; -use Gedmo\Mapping\MappedEventSubscriber; use Doctrine\Common\EventArgs; -use Gedmo\Mapping\Event\AdapterInterface; -use Gedmo\Exception\UploadablePartialException; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Event\ManagerEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\NotifyPropertyChanged; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Exception\InvalidArgumentException; use Gedmo\Exception\UploadableCantWriteException; +use Gedmo\Exception\UploadableCouldntGuessMimeTypeException; use Gedmo\Exception\UploadableExtensionException; +use Gedmo\Exception\UploadableFileAlreadyExistsException; use Gedmo\Exception\UploadableFormSizeException; use Gedmo\Exception\UploadableIniSizeException; +use Gedmo\Exception\UploadableInvalidMimeTypeException; +use Gedmo\Exception\UploadableMaxSizeException; use Gedmo\Exception\UploadableNoFileException; +use Gedmo\Exception\UploadableNoPathDefinedException; use Gedmo\Exception\UploadableNoTmpDirException; +use Gedmo\Exception\UploadablePartialException; use Gedmo\Exception\UploadableUploadException; -use Gedmo\Exception\UploadableFileAlreadyExistsException; -use Gedmo\Exception\UploadableNoPathDefinedException; -use Gedmo\Exception\UploadableMaxSizeException; -use Gedmo\Exception\UploadableInvalidMimeTypeException; -use Gedmo\Exception\UploadableCouldntGuessMimeTypeException; -use Gedmo\Uploadable\Mapping\Validator; +use Gedmo\Mapping\Event\AdapterInterface; +use Gedmo\Mapping\MappedEventSubscriber; +use Gedmo\Uploadable\Event\UploadablePostFileProcessEventArgs; +use Gedmo\Uploadable\Event\UploadablePreFileProcessEventArgs; +use Gedmo\Uploadable\FileInfo\FileInfoArray; use Gedmo\Uploadable\FileInfo\FileInfoInterface; +use Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorInterface; +use Gedmo\Uploadable\Mapping\Validator; use Gedmo\Uploadable\MimeType\MimeTypeGuesser; use Gedmo\Uploadable\MimeType\MimeTypeGuesserInterface; -use Doctrine\Common\NotifyPropertyChanged; -use Gedmo\Uploadable\Event\UploadablePreFileProcessEventArgs; -use Gedmo\Uploadable\Event\UploadablePostFileProcessEventArgs; /** * Uploadable listener * * @author Gustavo Falco * @author Gediminas Morkevicius - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @phpstan-type UploadableConfiguration = array{ + * filePathField?: string, + * uploadable?: bool, + * fileNameField?: string, + * allowOverwrite?: bool, + * appendNumber?: bool, + * maxSize?: float, + * path?: string, + * pathMethod?: string, + * allowedTypes?: string[], + * disallowedTypes?: string[], + * filenameGenerator?: Validator::FILENAME_GENERATOR_*|class-string, + * fileMimeTypeField?: string, + * fileSizeField?: string, + * callback?: string, + * } + * + * @phpstan-extends MappedEventSubscriber */ class UploadableListener extends MappedEventSubscriber { - const ACTION_INSERT = 'INSERT'; - const ACTION_UPDATE = 'UPDATE'; + public const ACTION_INSERT = 'INSERT'; + public const ACTION_UPDATE = 'UPDATE'; /** * Default path to move files in @@ -48,49 +80,51 @@ class UploadableListener extends MappedEventSubscriber /** * Mime type guesser - * - * @var \Gedmo\Uploadable\MimeType\MimeTypeGuesserInterface */ - private $mimeTypeGuesser; + private MimeTypeGuesserInterface $mimeTypeGuesser; /** * Default FileInfoInterface class * - * @var string + * @var class-string */ - private $defaultFileInfoClass = 'Gedmo\Uploadable\FileInfo\FileInfoArray'; + private string $defaultFileInfoClass = FileInfoArray::class; /** * Array of files to remove on postFlush * - * @var array + * @var array */ - private $pendingFileRemovals = array(); + private array $pendingFileRemovals = []; /** * Array of FileInfoInterface objects. The index is the hash of the entity owner * of the FileInfoInterface object. * - * @var array + * @var array> + * + * @phpstan-var array */ - private $fileInfoObjects = array(); + private array $fileInfoObjects = []; - public function __construct(MimeTypeGuesserInterface $mimeTypeGuesser = null) + public function __construct(?MimeTypeGuesserInterface $mimeTypeGuesser = null) { - $this->mimeTypeGuesser = $mimeTypeGuesser ? $mimeTypeGuesser : new MimeTypeGuesser(); + parent::__construct(); + + $this->mimeTypeGuesser = $mimeTypeGuesser ?? new MimeTypeGuesser(); } /** - * {@inheritdoc} + * @return string[] */ public function getSubscribedEvents() { - return array( + return [ 'loadClassMetadata', 'preFlush', 'onFlush', 'postFlush', - ); + ]; } /** @@ -99,11 +133,15 @@ public function getSubscribedEvents() * doctrine thinks the entity has no changes, which produces that the "onFlush" event gets never called. * Here we mark the entity as dirty, so the "onFlush" event gets called, and the file is processed. * - * @param \Doctrine\Common\EventArgs $args + * @param ManagerEventArgs $args + * + * @phpstan-param ManagerEventArgs $args + * + * @return void */ public function preFlush(EventArgs $args) { - if (empty($this->fileInfoObjects)) { + if ([] === $this->fileInfoObjects) { // Nothing to do return; } @@ -111,12 +149,11 @@ public function preFlush(EventArgs $args) $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); - $first = reset($this->fileInfoObjects); - $meta = $om->getClassMetadata(get_class($first['entity'])); - $config = $this->getConfiguration($om, $meta->name); foreach ($this->fileInfoObjects as $info) { $entity = $info['entity']; + $meta = $om->getClassMetadata(get_class($entity)); + $config = $this->getConfiguration($om, $meta->getName()); // If the entity is in the identity map, it means it will be updated. We need to force the // "dirty check" here by "modifying" the path. We are actually setting the same value, but @@ -139,7 +176,11 @@ public function preFlush(EventArgs $args) * Handle file-uploading depending on the action * being done with objects * - * @param \Doctrine\Common\EventArgs $args + * @param ManagerEventArgs $args + * + * @phpstan-param ManagerEventArgs $args + * + * @return void */ public function onFlush(EventArgs $args) { @@ -165,15 +206,9 @@ public function onFlush(EventArgs $args) foreach ($ea->getScheduledObjectDeletions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); - if ($config = $this->getConfiguration($om, $meta->name)) { + if ($config = $this->getConfiguration($om, $meta->getName())) { if (isset($config['uploadable']) && $config['uploadable']) { - if ($config['filePathField']) { - $this->pendingFileRemovals[] = $this->getFilePathFieldValue($meta, $config, $object); - } else { - $path = $this->getPath($meta, $config, $object); - $fileName = $this->getFileNameFieldValue($meta, $config, $object); - $this->pendingFileRemovals[] = $path.DIRECTORY_SEPARATOR.$fileName; - } + $this->addFileRemoval($meta, $config, $object); } } } @@ -182,41 +217,43 @@ public function onFlush(EventArgs $args) /** * Handle removal of files * - * @param \Doctrine\Common\EventArgs $args + * @return void */ public function postFlush(EventArgs $args) { - if (!empty($this->pendingFileRemovals)) { + if ([] !== $this->pendingFileRemovals) { foreach ($this->pendingFileRemovals as $file) { $this->removeFile($file); } - $this->pendingFileRemovals = array(); + $this->pendingFileRemovals = []; } - $this->fileInfoObjects = array(); + $this->fileInfoObjects = []; } /** * If it's a Uploadable object, verify if the file was uploaded. * If that's the case, process it. * - * @param \Gedmo\Mapping\Event\AdapterInterface $ea - * @param object $object - * @param string $action + * @param object $object + * @param string $action + * + * @throws UploadableNoPathDefinedException + * @throws UploadableCouldntGuessMimeTypeException + * @throws UploadableMaxSizeException + * @throws UploadableInvalidMimeTypeException * - * @throws \Gedmo\Exception\UploadableNoPathDefinedException - * @throws \Gedmo\Exception\UploadableCouldntGuessMimeTypeException - * @throws \Gedmo\Exception\UploadableMaxSizeException - * @throws \Gedmo\Exception\UploadableInvalidMimeTypeException + * @return void */ public function processFile(AdapterInterface $ea, $object, $action) { - $oid = spl_object_hash($object); + $oid = spl_object_id($object); $om = $ea->getObjectManager(); + \assert($om instanceof EntityManagerInterface); $uow = $om->getUnitOfWork(); $meta = $om->getClassMetadata(get_class($object)); - $config = $this->getConfiguration($om, $meta->name); + $config = $this->getConfiguration($om, $meta->getName()); if (!$config || !isset($config['uploadable']) || !$config['uploadable']) { // Nothing to do @@ -242,24 +279,18 @@ public function processFile(AdapterInterface $ea, $object, $action) if ($config['maxSize'] > 0 && $fileInfo->getSize() > $config['maxSize']) { $msg = 'File "%s" exceeds the maximum allowed size of %d bytes. File size: %d bytes'; - throw new UploadableMaxSizeException(sprintf($msg, - $fileInfo->getName(), - $config['maxSize'], - $fileInfo->getSize() - )); + throw new UploadableMaxSizeException(sprintf($msg, $fileInfo->getName(), $config['maxSize'], $fileInfo->getSize())); } $mime = $this->mimeTypeGuesser->guess($fileInfo->getTmpName()); - if (!$mime) { - throw new UploadableCouldntGuessMimeTypeException(sprintf('Couldn\'t guess mime type for file "%s".', - $fileInfo->getName() - )); + if (null === $mime) { + throw new UploadableCouldntGuessMimeTypeException(sprintf('Couldn\'t guess mime type for file "%s".', $fileInfo->getName())); } if ($config['allowedTypes'] || $config['disallowedTypes']) { $ok = $config['allowedTypes'] ? false : true; - $mimes = $config['allowedTypes'] ? $config['allowedTypes'] : $config['disallowedTypes']; + $mimes = $config['allowedTypes'] ?: $config['disallowedTypes']; foreach ($mimes as $m) { if ($mime === $m) { @@ -270,24 +301,15 @@ public function processFile(AdapterInterface $ea, $object, $action) } if (!$ok) { - throw new UploadableInvalidMimeTypeException(sprintf('Invalid mime type "%s" for file "%s".', - $mime, - $fileInfo->getName() - )); + throw new UploadableInvalidMimeTypeException(sprintf('Invalid mime type "%s" for file "%s".', $mime, $fileInfo->getName())); } } $path = $this->getPath($meta, $config, $object); - if ($action === self::ACTION_UPDATE) { + if (self::ACTION_UPDATE === $action) { // First we add the original file to the pendingFileRemovals array - if ($config['filePathField']) { - $this->pendingFileRemovals[] = $this->getFilePathFieldValue($meta, $config, $object); - } else { - $path = $this->getPath($meta, $config, $object); - $fileName = $this->getFileNameFieldValue($meta, $config, $object); - $this->pendingFileRemovals[] = $path.DIRECTORY_SEPARATOR.$fileName; - } + $this->addFileRemoval($meta, $config, $object); } // We generate the filename based on configuration @@ -315,11 +337,14 @@ public function processFile(AdapterInterface $ea, $object, $action) // We override the mime type with the guessed one $info['fileMimeType'] = $mime; - if ($config['callback'] !== '') { + if ('' !== $config['callback']) { $callbackMethod = $refl->getMethod($config['callback']); - $callbackMethod->setAccessible(true); - $callbackMethod->invokeArgs($object, array($info)); + if (PHP_VERSION_ID < 80100) { + $callbackMethod->setAccessible(true); + } + + $callbackMethod->invokeArgs($object, [$info]); } if ($config['filePathField']) { @@ -335,7 +360,11 @@ public function processFile(AdapterInterface $ea, $object, $action) } if ($config['fileSizeField']) { - $this->updateField($object, $uow, $ea, $meta, $config['fileSizeField'], $info['fileSize']); + $value = $om->getConnection()->convertToPHPValue( + $info['fileSize'], + $meta->getTypeOfField($config['fileSizeField']) + ); + $this->updateField($object, $uow, $ea, $meta, $config['fileSizeField'], $value); } $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); @@ -354,89 +383,6 @@ public function processFile(AdapterInterface $ea, $object, $action) unset($this->fileInfoObjects[$oid]); } - /** - * @param ClassMetadata $meta - * @param array $config - * @param object $object Entity - * - * @return string - * - * @throws UploadableNoPathDefinedException - */ - protected function getPath(ClassMetadata $meta, array $config, $object) - { - $path = $config['path']; - - if ($path === '') { - $defaultPath = $this->getDefaultPath(); - if ($config['pathMethod'] !== '') { - $pathMethod = $meta->getReflectionClass()->getMethod($config['pathMethod']); - $pathMethod->setAccessible(true); - $path = $pathMethod->invoke($object, $defaultPath); - } elseif ($defaultPath !== null) { - $path = $defaultPath; - } else { - $msg = 'You have to define the path to save files either in the listener, or in the class "%s"'; - - throw new UploadableNoPathDefinedException( - sprintf($msg, $meta->name) - ); - } - } - - Validator::validatePath($path); - $path = rtrim($path, '\/'); - - return $path; - } - - /** - * Returns value of the entity's property - * - * @param ClassMetadata $meta - * @param string $propertyName - * @param object $object - * - * @return mixed - */ - protected function getPropertyValueFromObject(ClassMetadata $meta, $propertyName, $object) - { - $refl = $meta->getReflectionClass(); - $filePathField = $refl->getProperty($propertyName); - $filePathField->setAccessible(true); - $filePath = $filePathField->getValue($object); - - return $filePath; - } - - /** - * Returns the path of the entity's file - * - * @param ClassMetadata $meta - * @param array $config - * @param object $object - * - * @return string - */ - protected function getFilePathFieldValue(ClassMetadata $meta, array $config, $object) - { - return $this->getPropertyValueFromObject($meta, $config['filePathField'], $object); - } - - /** - * Returns the name of the entity's file - * - * @param ClassMetadata $meta - * @param array $config - * @param object $object - * - * @return string - */ - protected function getFileNameFieldValue(ClassMetadata $meta, array $config, $object) - { - return $this->getPropertyValueFromObject($meta, $config['fileNameField'], $object); - } - /** * Simple wrapper for the function "unlink" to ease testing * @@ -456,26 +402,27 @@ public function removeFile($filePath) /** * Moves the file to the specified path * - * @param FileInfoInterface $fileInfo - * @param string $path - * @param bool $filenameGeneratorClass - * @param bool $overwrite - * @param bool $appendNumber - * @param object $object - * - * @return array - * - * @throws \Gedmo\Exception\UploadableUploadException - * @throws \Gedmo\Exception\UploadableNoFileException - * @throws \Gedmo\Exception\UploadableExtensionException - * @throws \Gedmo\Exception\UploadableIniSizeException - * @throws \Gedmo\Exception\UploadableFormSizeException - * @throws \Gedmo\Exception\UploadableFileAlreadyExistsException - * @throws \Gedmo\Exception\UploadablePartialException - * @throws \Gedmo\Exception\UploadableNoTmpDirException - * @throws \Gedmo\Exception\UploadableCantWriteException + * @param string $path + * @param string|bool $filenameGeneratorClass + * @param bool $overwrite + * @param bool $appendNumber + * @param object $object + * + * @phpstan-param class-string|false $filenameGeneratorClass + * + * @throws UploadableUploadException + * @throws UploadableNoFileException + * @throws UploadableExtensionException + * @throws UploadableIniSizeException + * @throws UploadableFormSizeException + * @throws UploadableFileAlreadyExistsException + * @throws UploadablePartialException + * @throws UploadableNoTmpDirException + * @throws UploadableCantWriteException + * + * @return array */ - public function moveFile(FileInfoInterface $fileInfo, $path, $filenameGeneratorClass = false, $overwrite = false, $appendNumber = false, $object) + public function moveFile(FileInfoInterface $fileInfo, $path, $filenameGeneratorClass = false, $overwrite = false, $appendNumber = false, $object = null) { if ($fileInfo->getError() > 0) { switch ($fileInfo->getError()) { @@ -494,7 +441,7 @@ public function moveFile(FileInfoInterface $fileInfo, $path, $filenameGeneratorC case 4: $msg = 'No file was uploaded!'; - throw new UploadableNoFileException(sprintf($msg, $fileInfo->getName())); + throw new UploadableNoFileException($msg); case 6: $msg = 'Upload failed. Temp dir is missing.'; @@ -506,23 +453,21 @@ public function moveFile(FileInfoInterface $fileInfo, $path, $filenameGeneratorC case 8: $msg = 'A PHP Extension stopped the uploaded for some reason.'; - throw new UploadableExtensionException(sprintf($msg, $fileInfo->getName())); + throw new UploadableExtensionException($msg); default: - throw new UploadableUploadException(sprintf('There was an unknown problem while uploading file "%s"', - $fileInfo->getName() - )); + throw new UploadableUploadException(sprintf('There was an unknown problem while uploading file "%s"', $fileInfo->getName())); } } - $info = array( - 'fileName' => '', - 'fileExtension' => '', - 'fileWithoutExt' => '', - 'origFileName' => '', - 'filePath' => '', - 'fileMimeType' => $fileInfo->getType(), - 'fileSize' => $fileInfo->getSize(), - ); + $info = [ + 'fileName' => '', + 'fileExtension' => '', + 'fileWithoutExt' => '', + 'origFileName' => '', + 'filePath' => '', + 'fileMimeType' => $fileInfo->getType(), + 'fileSize' => $fileInfo->getSize(), + ]; $info['fileName'] = basename($fileInfo->getName()); $info['filePath'] = $path.'/'.$info['fileName']; @@ -533,14 +478,14 @@ public function moveFile(FileInfoInterface $fileInfo, $path, $filenameGeneratorC $info['fileExtension'] = substr($info['filePath'], strrpos($info['filePath'], '.')); $info['fileWithoutExt'] = substr($info['filePath'], 0, strrpos($info['filePath'], '.')); } else { - $info['fileWithoutExt'] = $info['fileName']; + $info['fileWithoutExt'] = $info['filePath']; } // Save the original filename for later use $info['origFileName'] = $info['fileName']; // Now we generate the filename using the configured class - if ($filenameGeneratorClass) { + if (false !== $filenameGeneratorClass) { $filename = $filenameGeneratorClass::generate( str_replace($path.'/', '', $info['fileWithoutExt']), $info['fileExtension'], @@ -563,14 +508,7 @@ public function moveFile(FileInfoInterface $fileInfo, $path, $filenameGeneratorC if (is_file($info['filePath'])) { if ($overwrite) { - - $k = array_search($info['filePath'], $this->pendingFileRemovals); - - if ($k !== false) - { - unset($this->pendingFileRemovals[$k]); - } - + $this->cancelFileRemoval($info['filePath']); $this->removeFile($info['filePath']); } elseif ($appendNumber) { $counter = 1; @@ -580,17 +518,12 @@ public function moveFile(FileInfoInterface $fileInfo, $path, $filenameGeneratorC $info['filePath'] = $info['fileWithoutExt'].'-'.(++$counter).$info['fileExtension']; } while (is_file($info['filePath'])); } else { - throw new UploadableFileAlreadyExistsException(sprintf('File "%s" already exists!', - $info['filePath'] - )); + throw new UploadableFileAlreadyExistsException(sprintf('File "%s" already exists!', $info['filePath'])); } } if (!$this->doMoveFile($fileInfo->getTmpName(), $info['filePath'], $fileInfo->isUploadedFile())) { - throw new UploadableUploadException(sprintf('File "%s" was not uploaded, or there was a problem moving it to the location "%s".', - $fileInfo->getName(), - $path - )); + throw new UploadableUploadException(sprintf('File "%s" was not uploaded, or there was a problem moving it to the location "%s".', $fileInfo->getName(), $path)); } return $info; @@ -615,12 +548,15 @@ public function doMoveFile($source, $dest, $isUploadedFile = true) /** * Maps additional metadata * - * @param EventArgs $eventArgs + * @param LoadClassMetadataEventArgs $eventArgs + * + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $eventArgs + * + * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { - $ea = $this->getEventAdapter($eventArgs); - $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata()); + $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } /** @@ -638,7 +574,7 @@ public function setDefaultPath($path) /** * Returns default path * - * @return string + * @return string|null */ public function getDefaultPath() { @@ -654,17 +590,10 @@ public function getDefaultPath() */ public function setDefaultFileInfoClass($defaultFileInfoClass) { - $fileInfoInterface = 'Gedmo\\Uploadable\\FileInfo\\FileInfoInterface'; - $refl = is_string($defaultFileInfoClass) && class_exists($defaultFileInfoClass) ? - new \ReflectionClass($defaultFileInfoClass) : - false; - - if (!$refl || !$refl->implementsInterface($fileInfoInterface)) { - $msg = sprintf('Default FileInfo class must be a valid class, and it must implement "%s".', - $fileInfoInterface - ); - - throw new \Gedmo\Exception\InvalidArgumentException($msg); + if (!is_string($defaultFileInfoClass) || !class_exists($defaultFileInfoClass) + || !is_subclass_of($defaultFileInfoClass, FileInfoInterface::class) + ) { + throw new InvalidArgumentException(sprintf('Default FileInfo class must be a valid class, and it must implement "%s".', FileInfoInterface::class)); } $this->defaultFileInfoClass = $defaultFileInfoClass; @@ -673,7 +602,7 @@ public function setDefaultFileInfoClass($defaultFileInfoClass) /** * Returns file info default class * - * @return string + * @return class-string */ public function getDefaultFileInfoClass() { @@ -683,10 +612,12 @@ public function getDefaultFileInfoClass() /** * Adds a FileInfoInterface object for the given entity * - * @param object $entity - * @param array|FileInfoInterface $fileInfo + * @param object $entity + * @param array|FileInfoInterface $fileInfo * * @throws \RuntimeException + * + * @return void */ public function addEntityFileInfo($entity, $fileInfo) { @@ -699,10 +630,10 @@ public function addEntityFileInfo($entity, $fileInfo) throw new \RuntimeException(sprintf($msg, get_class($entity))); } - $this->fileInfoObjects[spl_object_hash($entity)] = array( - 'entity' => $entity, - 'fileInfo' => $fileInfo, - ); + $this->fileInfoObjects[spl_object_id($entity)] = [ + 'entity' => $entity, + 'fileInfo' => $fileInfo, + ]; } /** @@ -712,55 +643,159 @@ public function addEntityFileInfo($entity, $fileInfo) */ public function getEntityFileInfo($entity) { - $oid = spl_object_hash($entity); + $oid = spl_object_id($entity); if (!isset($this->fileInfoObjects[$oid])) { - throw new \RuntimeException(sprintf('There\'s no FileInfoInterface object for entity of class "%s".', - get_class($entity) - )); + throw new \RuntimeException(sprintf('There\'s no FileInfoInterface object for entity of class "%s".', get_class($entity))); } return $this->fileInfoObjects[$oid]['fileInfo']; } /** - * {@inheritDoc} + * @return void */ - protected function getNamespace() + public function setMimeTypeGuesser(MimeTypeGuesserInterface $mimeTypeGuesser) { - return __NAMESPACE__; + $this->mimeTypeGuesser = $mimeTypeGuesser; } /** - * @param \Gedmo\Uploadable\MimeType\MimeTypeGuesserInterface $mimeTypeGuesser + * @return MimeTypeGuesserInterface */ - public function setMimeTypeGuesser(MimeTypeGuesserInterface $mimeTypeGuesser) + public function getMimeTypeGuesser() { - $this->mimeTypeGuesser = $mimeTypeGuesser; + return $this->mimeTypeGuesser; } /** - * @return \Gedmo\Uploadable\MimeType\MimeTypeGuesserInterface + * @param ClassMetadata $meta + * @param array $config + * @param object $object Entity + * + * @throws UploadableNoPathDefinedException + * + * @return string */ - public function getMimeTypeGuesser() + protected function getPath(ClassMetadata $meta, array $config, $object) { - return $this->mimeTypeGuesser; + $path = $config['path']; + + if ('' === $path) { + $defaultPath = $this->getDefaultPath(); + if ('' !== $config['pathMethod']) { + $getPathMethod = \Closure::bind(fn (string $pathMethod, ?string $defaultPath): string => $this->{$pathMethod}($defaultPath), $object, $meta->getReflectionClass()->getName()); + + $path = $getPathMethod($config['pathMethod'], $defaultPath); + } elseif (null !== $defaultPath) { + $path = $defaultPath; + } else { + $msg = 'You have to define the path to save files either in the listener, or in the class "%s"'; + + throw new UploadableNoPathDefinedException(sprintf($msg, $meta->getName())); + } + } + + Validator::validatePath($path); + + return rtrim($path, '\/'); + } + + /** + * @param ClassMetadata $meta + * @param array $config + * @param object $object Entity + * + * @return void + */ + protected function addFileRemoval($meta, $config, $object) + { + if ($config['filePathField']) { + $this->pendingFileRemovals[] = $this->getFilePathFieldValue($meta, $config, $object); + } else { + $path = $this->getPath($meta, $config, $object); + $fileName = $this->getFileNameFieldValue($meta, $config, $object); + $this->pendingFileRemovals[] = $path.DIRECTORY_SEPARATOR.$fileName; + } + } + + /** + * @param string $filePath + * + * @return void + */ + protected function cancelFileRemoval($filePath) + { + $k = array_search($filePath, $this->pendingFileRemovals, true); + + if (false !== $k) { + unset($this->pendingFileRemovals[$k]); + } + } + + /** + * Returns value of the entity's property + * + * @param ClassMetadata $meta + * @param string $propertyName + * @param object $object + * + * @return mixed + */ + protected function getPropertyValueFromObject(ClassMetadata $meta, $propertyName, $object) + { + $getFilePath = \Closure::bind(fn (string $propertyName) => $this->{$propertyName}, $object, $meta->getReflectionClass()->getName()); + + return $getFilePath($propertyName); } /** - * @param object $object - * @param object $uow - * @param AdapterInterface $ea - * @param ClassMetadata $meta - * @param String $field - * @param mixed $value - * @param bool $notifyPropertyChanged + * Returns the path of the entity's file + * + * @param ClassMetadata $meta + * @param array $config + * @param object $object + * + * @return string + */ + protected function getFilePathFieldValue(ClassMetadata $meta, array $config, $object) + { + return $this->getPropertyValueFromObject($meta, $config['filePathField'], $object); + } + + /** + * Returns the name of the entity's file + * + * @param ClassMetadata $meta + * @param array $config + * @param object $object + * + * @return string + */ + protected function getFileNameFieldValue(ClassMetadata $meta, array $config, $object) + { + return $this->getPropertyValueFromObject($meta, $config['fileNameField'], $object); + } + + protected function getNamespace() + { + return __NAMESPACE__; + } + + /** + * @param object $object + * @param object $uow + * @param ClassMetadata $meta + * @param string $field + * @param mixed $value + * @param bool $notifyPropertyChanged + * + * @return void */ protected function updateField($object, $uow, AdapterInterface $ea, ClassMetadata $meta, $field, $value, $notifyPropertyChanged = true) { - $property = $meta->getReflectionProperty($field); - $oldValue = $property->getValue($object); - $property->setValue($object, $value); + $oldValue = $meta->getFieldValue($object, $field); + $meta->setFieldValue($object, $field, $value); if ($notifyPropertyChanged && $object instanceof NotifyPropertyChanged) { $uow = $ea->getObjectManager()->getUnitOfWork(); diff --git a/tests/Gedmo/Blameable/BlameableDocumentTest.php b/tests/Gedmo/Blameable/BlameableDocumentTest.php index bec3497cac..15578b92b3 100644 --- a/tests/Gedmo/Blameable/BlameableDocumentTest.php +++ b/tests/Gedmo/Blameable/BlameableDocumentTest.php @@ -1,29 +1,33 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Blameable\Fixture\Document\Article; -use Blameable\Fixture\Document\Type; -use Blameable\Fixture\Document\User; +use Gedmo\Blameable\BlameableListener; +use Gedmo\Tests\Blameable\Fixture\Document\Article; +use Gedmo\Tests\Blameable\Fixture\Document\Type; +use Gedmo\Tests\Blameable\Fixture\Document\User; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for Blameable behavior ODM implementation * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class BlameableDocumentTest extends BaseTestCaseMongoODM +final class BlameableDocumentTest extends BaseTestCaseMongoODM { - const TEST_USERNAME = 'testuser'; - - const TYPE = 'Blameable\Fixture\Document\Type'; - const USER = 'Blameable\Fixture\Document\User'; - const ARTICLE = 'Blameable\Fixture\Document\Article'; + private const TEST_USERNAME = 'testuser'; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -36,19 +40,19 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($listener); - $manager = $this->getMockDocumentManager($evm); + $manager = $this->getDefaultDocumentManager($evm); $manager->persist($user); $this->populate(); $manager->flush(); } - public function testBlameable() + public function testBlameable(): void { - $repo = $this->dm->getRepository(self::ARTICLE); - $article = $repo->findOneByTitle('Blameable Article'); + $repo = $this->dm->getRepository(Article::class); + $article = $repo->findOneBy(['title' => 'Blameable Article']); - $this->assertEquals(self::TEST_USERNAME, $article->getCreated()); - $this->assertEquals(self::TEST_USERNAME, $article->getUpdated()); + static::assertSame(self::TEST_USERNAME, $article->getCreated()); + static::assertSame(self::TEST_USERNAME, $article->getUpdated()); $published = new Type(); $published->setIdentifier('published'); @@ -59,13 +63,13 @@ public function testBlameable() $this->dm->persist($published); $this->dm->flush(); - $article = $repo->findOneByTitle('Blameable Article'); + $article = $repo->findOneBy(['title' => 'Blameable Article']); - $this->assertEquals(self::TEST_USERNAME, $article->getPublished()); - $this->assertEquals(self::TEST_USERNAME, $article->getCreator()->getUsername()); + static::assertSame(self::TEST_USERNAME, $article->getPublished()); + static::assertSame(self::TEST_USERNAME, $article->getCreator()->getUsername()); } - public function testForcedValues() + public function testForcedValues(): void { $sport = new Article(); $sport->setTitle('sport forced'); @@ -75,10 +79,10 @@ public function testForcedValues() $this->dm->persist($sport); $this->dm->flush(); - $repo = $this->dm->getRepository(self::ARTICLE); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals(self::TEST_USERNAME, $sport->getCreated()); - $this->assertEquals(self::TEST_USERNAME, $sport->getUpdated()); + $repo = $this->dm->getRepository(Article::class); + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame(self::TEST_USERNAME, $sport->getCreated()); + static::assertSame(self::TEST_USERNAME, $sport->getUpdated()); $published = new Type(); $published->setIdentifier('published'); @@ -90,11 +94,11 @@ public function testForcedValues() $this->dm->persist($published); $this->dm->flush(); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals(self::TEST_USERNAME, $sport->getPublished()); + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame(self::TEST_USERNAME, $sport->getPublished()); } - private function populate() + private function populate(): void { $art0 = new Article(); $art0->setTitle('Blameable Article'); diff --git a/tests/Gedmo/Blameable/BlameableTest.php b/tests/Gedmo/Blameable/BlameableTest.php index c35c86a7ff..b233150a98 100644 --- a/tests/Gedmo/Blameable/BlameableTest.php +++ b/tests/Gedmo/Blameable/BlameableTest.php @@ -1,66 +1,113 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Blameable\Fixture\Entity\Article; -use Blameable\Fixture\Entity\Comment; -use Blameable\Fixture\Entity\Type; +use Gedmo\Blameable\BlameableListener; +use Gedmo\Tests\Blameable\Fixture\Entity\Article; +use Gedmo\Tests\Blameable\Fixture\Entity\Comment; +use Gedmo\Tests\Blameable\Fixture\Entity\Type; +use Gedmo\Tests\TestActorProvider; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Blameable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class BlameableTest extends BaseTestCaseORM +final class BlameableTest extends BaseTestCaseORM { - const ARTICLE = "Blameable\\Fixture\\Entity\\Article"; - const COMMENT = "Blameable\\Fixture\\Entity\\Comment"; - const TYPE = "Blameable\\Fixture\\Entity\\Type"; + private BlameableListener $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); - $listener = new BlameableListener(); - $listener->setUserValue('testuser'); + $this->listener = new BlameableListener(); + $this->listener->setUserValue('testuser'); $evm = new EventManager(); - $evm->addEventSubscriber($listener); + $evm->addEventSubscriber($this->listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testBlameable() + public function testBlameable(): void { $sport = new Article(); $sport->setTitle('Sport'); - $this->assertTrue($sport instanceof Blameable); - $sportComment = new Comment(); $sportComment->setMessage('hello'); $sportComment->setArticle($sport); $sportComment->setStatus(0); - $this->assertTrue($sportComment instanceof Blameable); + $this->em->persist($sport); + $this->em->persist($sportComment); + $this->em->flush(); + $this->em->clear(); + + $sport = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); + static::assertSame('testuser', $sport->getCreated()); + static::assertSame('testuser', $sport->getUpdated()); + static::assertNull($sport->getPublished()); + + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertSame('testuser', $sportComment->getModified()); + static::assertNull($sportComment->getClosed()); + + $sportComment->setStatus(1); + $published = new Type(); + $published->setTitle('Published'); + + $sport->setTitle('Updated'); + $sport->setType($published); + $this->em->persist($sport); + $this->em->persist($published); + $this->em->persist($sportComment); + $this->em->flush(); + $this->em->clear(); + + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertSame('testuser', $sportComment->getClosed()); + + static::assertSame('testuser', $sport->getPublished()); + } + + public function testBlameableWithActorProvider(): void + { + $this->listener->setActorProvider(new TestActorProvider('testactor')); + + $sport = new Article(); + $sport->setTitle('Sport'); + + $sportComment = new Comment(); + $sportComment->setMessage('hello'); + $sportComment->setArticle($sport); + $sportComment->setStatus(0); $this->em->persist($sport); $this->em->persist($sportComment); $this->em->flush(); $this->em->clear(); - $sport = $this->em->getRepository(self::ARTICLE)->findOneByTitle('Sport'); - $this->assertEquals('testuser', $sport->getCreated()); - $this->assertEquals('testuser', $sport->getUpdated()); - $this->assertNull($sport->getPublished()); + $sport = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); + static::assertSame('testactor', $sport->getCreated()); + static::assertSame('testactor', $sport->getUpdated()); + static::assertNull($sport->getPublished()); - $sportComment = $this->em->getRepository(self::COMMENT)->findOneByMessage('hello'); - $this->assertEquals('testuser', $sportComment->getModified()); - $this->assertNull($sportComment->getClosed()); + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertSame('testactor', $sportComment->getModified()); + static::assertNull($sportComment->getClosed()); $sportComment->setStatus(1); $published = new Type(); @@ -74,13 +121,13 @@ public function testBlameable() $this->em->flush(); $this->em->clear(); - $sportComment = $this->em->getRepository(self::COMMENT)->findOneByMessage('hello'); - $this->assertEquals('testuser', $sportComment->getClosed()); + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertSame('testactor', $sportComment->getClosed()); - $this->assertEquals('testuser', $sport->getPublished()); + static::assertSame('testactor', $sport->getPublished()); } - public function testForcedValues() + public function testForcedValues(): void { $sport = new Article(); $sport->setTitle('sport forced'); @@ -91,10 +138,10 @@ public function testForcedValues() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::ARTICLE); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals('myuser', $sport->getCreated()); - $this->assertEquals('myuser', $sport->getUpdated()); + $repo = $this->em->getRepository(Article::class); + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame('myuser', $sport->getCreated()); + static::assertSame('myuser', $sport->getUpdated()); $published = new Type(); $published->setTitle('Published'); @@ -106,16 +153,16 @@ public function testForcedValues() $this->em->flush(); $this->em->clear(); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals('myuser', $sport->getPublished()); + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame('myuser', $sport->getPublished()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::COMMENT, - self::TYPE, - ); + return [ + Article::class, + Comment::class, + Type::class, + ]; } } diff --git a/tests/Gedmo/Blameable/BlameableUuidTest.php b/tests/Gedmo/Blameable/BlameableUuidTest.php new file mode 100644 index 0000000000..9bffdcb162 --- /dev/null +++ b/tests/Gedmo/Blameable/BlameableUuidTest.php @@ -0,0 +1,65 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable; + +use Doctrine\Common\EventManager; +use Gedmo\Blameable\BlameableListener; +use Gedmo\Tests\Blameable\Fixture\Entity\Company; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Symfony\Component\Uid\Uuid; +use Symfony\Component\Uid\UuidV6; + +final class BlameableUuidTest extends BaseTestCaseORM +{ + private UuidV6 $uuid; + + protected function setUp(): void + { + parent::setUp(); + + $this->uuid = Uuid::v6(); + + $listener = new BlameableListener(); + $listener->setUserValue($this->uuid); + + $evm = new EventManager(); + $evm->addEventSubscriber($listener); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testBlameableUuid(): void + { + $company = new Company(); + $company->setName('ACME'); + + $this->em->persist($company); + $this->em->flush(); + $this->em->clear(); + + /** + * @var Company $foundCompany + */ + $foundCompany = $this->em->getRepository(Company::class)->findOneBy(['name' => 'ACME']); + $created = $foundCompany->getCreated(); + $createdUuid = $created instanceof Uuid ? $created->toRfc4122() : null; + + static::assertSame($this->uuid->toRfc4122(), $createdUuid); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Company::class, + ]; + } +} diff --git a/tests/Gedmo/Blameable/ChangeTest.php b/tests/Gedmo/Blameable/ChangeTest.php index 035ce3c260..c221d4d535 100644 --- a/tests/Gedmo/Blameable/ChangeTest.php +++ b/tests/Gedmo/Blameable/ChangeTest.php @@ -1,25 +1,31 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Blameable\Fixture\Entity\TitledArticle; +use Gedmo\Blameable\BlameableListener; +use Gedmo\Tests\Blameable\Fixture\Entity\TitledArticle; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Blameable behavior * * @author Ivan Borzenkov - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ChangeTest extends BaseTestCaseORM +final class ChangeTest extends BaseTestCaseORM { - const FIXTURE = "Blameable\\Fixture\\Entity\\TitledArticle"; - - private $listener; + private BlameableListener $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -28,10 +34,10 @@ protected function setUp() $this->listener->setUserValue('testuser'); $evm->addEventSubscriber($this->listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testChange() + public function testChange(): void { $test = new TitledArticle(); $test->setTitle('Test'); @@ -41,29 +47,29 @@ public function testChange() $this->em->flush(); $this->em->clear(); - $test = $this->em->getRepository(self::FIXTURE)->findOneByTitle('Test'); + $test = $this->em->getRepository(TitledArticle::class)->findOneBy(['title' => 'Test']); $test->setTitle('New Title'); $this->em->persist($test); $this->em->flush(); $this->em->clear(); - //Changed - $this->assertEquals('testuser', $test->getChtitle()); + // Changed + static::assertSame('testuser', $test->getChtitle()); $this->listener->setUserValue('otheruser'); - $test = $this->em->getRepository(self::FIXTURE)->findOneByTitle('New Title'); + $test = $this->em->getRepository(TitledArticle::class)->findOneBy(['title' => 'New Title']); $test->setText('New Text'); $this->em->persist($test); $this->em->flush(); $this->em->clear(); - //Not Changed - $this->assertEquals('testuser', $test->getChtitle()); + // Not Changed + static::assertSame('testuser', $test->getChtitle()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::FIXTURE, - ); + return [ + TitledArticle::class, + ]; } } diff --git a/tests/Gedmo/Blameable/Fixture/Document/Article.php b/tests/Gedmo/Blameable/Fixture/Document/Article.php index bc79ea8f2e..796923e552 100644 --- a/tests/Gedmo/Blameable/Fixture/Document/Article.php +++ b/tests/Gedmo/Blameable/Fixture/Document/Article.php @@ -1,119 +1,143 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { - /** @ODM\Id */ + /** + * @ODM\Id + * + * @var string|null + */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceOne(targetDocument="Type") + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\Blameable\Fixture\Document\Type") */ - private $type; + #[ODM\ReferenceOne(targetDocument: Type::class)] + private ?Type $type = null; /** - * @var string $created + * @ODM\Field(type="string") * - * @ODM\String * @Gedmo\Blameable(on="create") */ - private $created; + #[ODM\Field(type: MongoDBType::STRING)] + #[Gedmo\Blameable(on: 'create')] + private ?string $created = null; /** - * @var string $updated + * @ODM\Field(type="string") * - * @ODM\String * @Gedmo\Blameable */ - private $updated; + #[ODM\Field(type: MongoDBType::STRING)] + #[Gedmo\Blameable] + private ?string $updated = null; /** - * @ODM\ReferenceOne(targetDocument="User") + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\Blameable\Fixture\Document\User") + * * @Gedmo\Blameable(on="create") */ - private $creator; + #[ODM\ReferenceOne(targetDocument: User::class)] + #[Gedmo\Blameable(on: 'create')] + private ?User $creator = null; /** - * @var string $published + * @ODM\Field(type="string") * - * @ODM\String * @Gedmo\Blameable(on="change", field="type.title", value="Published") */ - private $published; + #[Gedmo\Blameable(on: 'change', field: 'type.title', value: 'Published')] + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $published = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getCreated() + public function getCreated(): ?string { return $this->created; } - public function getPublished() + public function getPublished(): ?string { return $this->published; } - public function getCreator() + public function getCreator(): ?User { return $this->creator; } - public function getUpdated() + public function getUpdated(): ?string { return $this->updated; } - public function setType(Type $type) + public function setType(Type $type): void { $this->type = $type; } - public function getType() + public function getType(): ?Type { return $this->type; } - public function setCreated($created) + public function setCreated(?string $created): void { $this->created = $created; } - public function setPublished($published) + public function setPublished(?string $published): void { $this->published = $published; } - public function setUpdated($updated) + public function setUpdated(?string $updated): void { $this->updated = $updated; } - public function setCreator($creator) + public function setCreator(?User $creator): void { $this->creator = $creator; } diff --git a/tests/Gedmo/Blameable/Fixture/Document/Type.php b/tests/Gedmo/Blameable/Fixture/Document/Type.php index 23bc3cbaa8..e4b5425004 100644 --- a/tests/Gedmo/Blameable/Fixture/Document/Type.php +++ b/tests/Gedmo/Blameable/Fixture/Document/Type.php @@ -1,48 +1,66 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="types") */ +#[ODM\Document(collection: 'types')] class Type { - /** @ODM\Id */ + /** + * @ODM\Id + * + * @var string|null + */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $identifier; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $identifier = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getIdentifier() + public function getIdentifier(): ?string { return $this->identifier; } - public function setIdentifier($identifier) + public function setIdentifier(?string $identifier): void { $this->identifier = $identifier; } diff --git a/tests/Gedmo/Blameable/Fixture/Document/User.php b/tests/Gedmo/Blameable/Fixture/Document/User.php index 93c668326b..6a2255fa39 100644 --- a/tests/Gedmo/Blameable/Fixture/Document/User.php +++ b/tests/Gedmo/Blameable/Fixture/Document/User.php @@ -1,33 +1,50 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="users") */ +#[ODM\Document(collection: 'users')] class User { - /** @ODM\Id */ + /** + * @ODM\Id + * + * @var string|null + */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $username; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $username = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setUsername($username) + public function setUsername(?string $username): void { $this->username = $username; } - public function getUsername() + public function getUsername(): ?string { return $this->username; } diff --git a/tests/Gedmo/Blameable/Fixture/Entity/Article.php b/tests/Gedmo/Blameable/Fixture/Entity/Article.php index 77bf6358df..0ecb4263d5 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/Article.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/Article.php @@ -1,124 +1,153 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; use Gedmo\Blameable\Blameable; use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Article implements Blameable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** - * @ORM\OneToMany(targetEntity="Blameable\Fixture\Entity\Comment", mappedBy="article") + * @var Collection + * + * @ORM\OneToMany(targetEntity="Gedmo\Tests\Blameable\Fixture\Entity\Comment", mappedBy="article") */ - private $comments; + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article')] + private Collection $comments; /** - * @var string $created - * * @Gedmo\Blameable(on="create") + * * @ORM\Column(name="created", type="string") */ - private $created; + #[ORM\Column(name: 'created', type: Types::STRING)] + #[Gedmo\Blameable(on: 'create')] + private ?string $created = null; /** - * @var string $updated - * * @ORM\Column(name="updated", type="string") + * * @Gedmo\Blameable */ - private $updated; + #[Gedmo\Blameable] + #[ORM\Column(name: 'updated', type: Types::STRING)] + private ?string $updated = null; /** - * @var string $published - * * @ORM\Column(name="published", type="string", nullable=true) + * * @Gedmo\Blameable(on="change", field="type.title", value="Published") */ - private $published; + #[ORM\Column(name: 'published', type: Types::STRING, nullable: true)] + #[Gedmo\Blameable(on: 'change', field: 'type.title', value: 'Published')] + private ?string $published = null; /** * @ORM\ManyToOne(targetEntity="Type", inversedBy="articles") */ - private $type; + #[ORM\ManyToOne(targetEntity: Type::class, inversedBy: 'articles')] + private ?Type $type = null; - public function setType($type) + public function __construct() + { + $this->comments = new ArrayCollection(); + } + + public function setType(?Type $type): void { $this->type = $type; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function addComment(Comment $comment) + public function addComment(Comment $comment): void { $comment->setArticle($this); $this->comments[] = $comment; } - public function getComments() + /** + * @return Collection + */ + public function getComments(): Collection { return $this->comments; } - /** - * Get created - * - * @return string $created - */ - public function getCreated() + public function getCreated(): ?string { return $this->created; } - public function setCreated($created) + public function setCreated(?string $created): void { $this->created = $created; } - public function getPublished() + public function getPublished(): ?string { return $this->published; } - public function setPublished($published) + public function setPublished(?string $published): void { $this->published = $published; } - /** - * Get updated - * - * @return string $updated - */ - public function getUpdated() + public function getUpdated(): ?string { return $this->updated; } - public function setUpdated($updated) + public function setUpdated(?string $updated): void { $this->updated = $updated; } diff --git a/tests/Gedmo/Blameable/Fixture/Entity/Comment.php b/tests/Gedmo/Blameable/Fixture/Entity/Comment.php index 338f80f104..6d93430f3b 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/Comment.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/Comment.php @@ -1,86 +1,115 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; use Gedmo\Blameable\Blameable; use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Comment implements Blameable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="message", type="text") */ - private $message; + #[ORM\Column(name: 'message', type: Types::TEXT)] + private ?string $message = null; /** - * @ORM\ManyToOne(targetEntity="Blameable\Fixture\Entity\Article", inversedBy="comments") + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Blameable\Fixture\Entity\Article", inversedBy="comments") */ - private $article; + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'comments')] + private ?Article $article = null; /** * @ORM\Column(type="integer") */ - private $status; + #[ORM\Column(type: Types::INTEGER)] + private ?int $status = null; /** - * @var string $closed + * @var string|null * * @ORM\Column(name="closed", type="string", nullable=true) + * * @Gedmo\Blameable(on="change", field="status", value=1) */ + #[ORM\Column(name: 'closed', type: Types::STRING, nullable: true)] + #[Gedmo\Blameable(on: 'change', field: 'status', value: 1)] private $closed; /** - * @var string $modified + * @var string|null * * @ORM\Column(name="modified", type="string") + * * @Gedmo\Blameable(on="update") */ + #[ORM\Column(name: 'modified', type: Types::STRING)] + #[Gedmo\Blameable(on: 'update')] private $modified; - public function setArticle($article) + public function setArticle(?Article $article): void { $this->article = $article; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setStatus($status) + public function setStatus(?int $status): void { $this->status = $status; } - public function getStatus() + public function getStatus(): ?int { return $this->status; } - public function setMessage($message) + public function setMessage(?string $message): void { $this->message = $message; } - public function getMessage() + public function getMessage(): ?string { return $this->message; } - public function getModified() + public function getModified(): ?string { return $this->modified; } - public function getClosed() + public function getClosed(): ?string { return $this->closed; } diff --git a/tests/Gedmo/Blameable/Fixture/Entity/Company.php b/tests/Gedmo/Blameable/Fixture/Entity/Company.php new file mode 100644 index 0000000000..9c7d411f39 --- /dev/null +++ b/tests/Gedmo/Blameable/Fixture/Entity/Company.php @@ -0,0 +1,80 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Blameable\Blameable; +use Gedmo\Mapping\Annotation as Gedmo; +use Symfony\Component\Uid\Uuid; +use Symfony\Component\Uid\UuidV6; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class Company implements Blameable +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="name", type="string", length=128) + */ + #[ORM\Column(name: 'name', type: Types::STRING, length: 128)] + private ?string $name = null; + + /** + * @var UuidV6|string|null + * + * @Gedmo\Blameable(on="create") + * + * @ORM\Column(name="created", type="uuid") + */ + #[ORM\Column(name: 'created', type: 'uuid')] + #[Gedmo\Blameable(on: 'create')] + private $created; + + public function getId(): ?int + { + return $this->id; + } + + public function setName(?string $name): void + { + $this->name = $name; + } + + public function getName(): ?string + { + return $this->name; + } + + public function getCreated(): ?Uuid + { + return $this->created; + } + + public function setCreated(?UuidV6 $created): void + { + $this->created = $created; + } +} diff --git a/tests/Gedmo/Blameable/Fixture/Entity/MappedSupperClass.php b/tests/Gedmo/Blameable/Fixture/Entity/MappedSupperClass.php index fdd14ed584..55f1f2c8de 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/MappedSupperClass.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/MappedSupperClass.php @@ -1,84 +1,87 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** -* @ORM\MappedSuperclass -*/ + * @ORM\MappedSuperclass + */ +#[ORM\MappedSuperclass] class MappedSupperClass { /** - * @var integer $id - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - */ + * @var int|null + * + * @ORM\Column(name="id", type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] protected $id; /** - * @var string $locale - * - * @Gedmo\Locale - */ + * @var string|null + * + * @Gedmo\Locale + */ + #[Gedmo\Locale] protected $locale; /** - * @var string $title - * - * @Gedmo\Translatable - * @ORM\Column(name="name", type="string", length=255) - */ + * @var string|null + * + * @Gedmo\Translatable + * + * @ORM\Column(name="name", type="string", length=191) + */ + #[Gedmo\Translatable] + #[ORM\Column(name: 'name', type: Types::STRING, length: 191)] protected $name; /** - * @var string $createdBy - * - * @ORM\Column(name="created_by", type="string") - * @Gedmo\Blameable(on="create") - */ + * @var string|null + * + * @ORM\Column(name="created_by", type="string") + * + * @Gedmo\Blameable(on="create") + */ + #[ORM\Column(name: 'created_by', type: Types::STRING)] + #[Gedmo\Blameable(on: 'create')] protected $createdBy; /** - * Get id - * - * @return integer $id - * @codeCoverageIgnore - */ - public function getId() + * @codeCoverageIgnore + */ + public function getId(): ?int { return $this->id; } - /** - * Set name - * - * @param string $name - */ - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - /** - * Get name - * - * @return string $name - */ - public function getName() + public function getName(): ?string { return $this->name; } - /** - * Get createdBy - * - * @return string $createdBy - */ - public function getCreatedBy() + public function getCreatedBy(): ?string { return $this->createdBy; } diff --git a/tests/Gedmo/Blameable/Fixture/Entity/SupperClassExtension.php b/tests/Gedmo/Blameable/Fixture/Entity/SupperClassExtension.php index c852ee7262..af9d10241e 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/SupperClassExtension.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/SupperClassExtension.php @@ -1,27 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Entity; -use Gedmo\Mapping\Annotation as Gedmo; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class SupperClassExtension extends MappedSupperClass { /** * @ORM\Column(length=128) + * * @Gedmo\Translatable */ - private $title; + #[ORM\Column(length: 128)] + #[Gedmo\Translatable] + private ?string $title = null; - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Blameable/Fixture/Entity/TitledArticle.php b/tests/Gedmo/Blameable/Fixture/Entity/TitledArticle.php index 025d14afb1..b94f43408f 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/TitledArticle.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/TitledArticle.php @@ -1,13 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; use Gedmo\Blameable\Blameable; use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class TitledArticle implements Blameable { /** @@ -15,92 +27,87 @@ class TitledArticle implements Blameable * @ORM\GeneratedValue * @ORM\Column(type="integer") */ - private $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; /** * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** * @ORM\Column(name="text", type="string", length=128) */ - private $text; + #[ORM\Column(name: 'text', type: Types::STRING, length: 128)] + private ?string $text = null; /** - * @var string $updated - * * @ORM\Column(name="chtext", type="string", nullable=true) + * * @Gedmo\Blameable(on="change", field="text") */ - private $chtext; + #[ORM\Column(name: 'chtext', type: Types::STRING, nullable: true)] + #[Gedmo\Blameable(on: 'change', field: 'text')] + private ?string $chtext = null; /** - * @var string $chtitle - * * @ORM\Column(name="chtitle", type="string", nullable=true) + * * @Gedmo\Blameable(on="change", field="title") */ - private $chtitle; + #[ORM\Column(name: 'chtitle', type: Types::STRING, nullable: true)] + #[Gedmo\Blameable(on: 'change', field: 'title')] + private ?string $chtitle = null; - /** - * @param string $chtext - */ - public function setChtext($chtext) + public function setChtext(?string $chtext): void { $this->chtext = $chtext; } - /** - * @return string - */ - public function getChtext() + public function getChtext(): ?string { return $this->chtext; } - /** - * @param string $chtitle - */ - public function setChtitle($chtitle) + public function setChtitle(?string $chtitle): void { $this->chtitle = $chtitle; } - /** - * @return string - */ - public function getChtitle() + public function getChtitle(): ?string { return $this->chtitle; } - public function setId($id) + public function setId(?int $id): void { $this->id = $id; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setText($text) + public function setText(?string $text): void { $this->text = $text; } - public function getText() + public function getText(): ?string { return $this->text; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Blameable/Fixture/Entity/Type.php b/tests/Gedmo/Blameable/Fixture/Entity/Type.php index 4874174053..70149d5907 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/Type.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/Type.php @@ -1,37 +1,69 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Type { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="type") */ - private $articles; + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'type')] + private Collection $articles; + + public function __construct() + { + $this->articles = new ArrayCollection(); + } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Blameable/Fixture/Entity/UsingTrait.php b/tests/Gedmo/Blameable/Fixture/Entity/UsingTrait.php index 893f765995..e4de7d04af 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/UsingTrait.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/UsingTrait.php @@ -1,43 +1,61 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Blameable\Traits\BlameableEntity; /** * @ORM\Entity */ +#[ORM\Entity] class UsingTrait { - /** + /* * Hook Blameable behavior * updates createdAt, updatedAt fields */ use BlameableEntity; /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(length: 128)] + private ?string $title = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Blameable/Fixture/Entity/WithoutInterface.php b/tests/Gedmo/Blameable/Fixture/Entity/WithoutInterface.php index 0c93281ba9..653d9966ac 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/WithoutInterface.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/WithoutInterface.php @@ -1,55 +1,87 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class WithoutInterface { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(type="string", length=128) */ - private $title; + #[ORM\Column(type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Blameable(on="create") + * * @ORM\Column(type="string") */ + #[ORM\Column(type: Types::STRING)] + #[Gedmo\Blameable(on: 'create')] private $created; /** + * @var string|null + * * @ORM\Column(type="string") + * * @Gedmo\Blameable(on="update") */ + #[ORM\Column(type: Types::STRING)] + #[Gedmo\Blameable(on: 'update')] private $updated; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getCreated() + public function getCreated(): ?string { return $this->created; } - public function getUpdated() + public function getUpdated(): ?string { return $this->updated; } diff --git a/tests/Gedmo/Blameable/NoInterfaceTest.php b/tests/Gedmo/Blameable/NoInterfaceTest.php index 18f3959f3a..64ec2d8cdb 100644 --- a/tests/Gedmo/Blameable/NoInterfaceTest.php +++ b/tests/Gedmo/Blameable/NoInterfaceTest.php @@ -1,23 +1,29 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Blameable\Fixture\Entity\WithoutInterface; +use Gedmo\Blameable\BlameableListener; +use Gedmo\Tests\Blameable\Fixture\Entity\WithoutInterface; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Blameable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class NoInterfaceTest extends BaseTestCaseORM +final class NoInterfaceTest extends BaseTestCaseORM { - const FIXTURE = "Blameable\\Fixture\\Entity\\WithoutInterface"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -26,10 +32,10 @@ protected function setUp() $blameableListener->setUserValue('testuser'); $evm->addEventSubscriber($blameableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testBlameableNoInterface() + public function testBlameableNoInterface(): void { $test = new WithoutInterface(); $test->setTitle('Test'); @@ -38,15 +44,15 @@ public function testBlameableNoInterface() $this->em->flush(); $this->em->clear(); - $test = $this->em->getRepository(self::FIXTURE)->findOneByTitle('Test'); - $this->assertEquals('testuser', $test->getCreated()); - $this->assertEquals('testuser', $test->getUpdated()); + $test = $this->em->getRepository(WithoutInterface::class)->findOneBy(['title' => 'Test']); + static::assertSame('testuser', $test->getCreated()); + static::assertSame('testuser', $test->getUpdated()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::FIXTURE, - ); + return [ + WithoutInterface::class, + ]; } } diff --git a/tests/Gedmo/Blameable/NoUserTest.php b/tests/Gedmo/Blameable/NoUserTest.php index f3b21671fb..d731cfe26c 100644 --- a/tests/Gedmo/Blameable/NoUserTest.php +++ b/tests/Gedmo/Blameable/NoUserTest.php @@ -1,22 +1,29 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseMongoODM; -use Blameable\Fixture\Document\Article; +use Gedmo\Blameable\BlameableListener; +use Gedmo\Tests\Blameable\Fixture\Document\Article; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for Blameable behavior, when no user is available * * @author Kรฉvin Gomez - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class NoUserTest extends BaseTestCaseMongoODM +final class NoUserTest extends BaseTestCaseMongoODM { - const ARTICLE = 'Blameable\Fixture\Document\Article'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -26,10 +33,10 @@ protected function setUp() $evm->addEventSubscriber($listener); // create the document manager - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); } - public function testWhenNoUserIsAvailable() + public function testWhenNoUserIsAvailable(): void { $sport = new Article(); $sport->setTitle('sport no user'); @@ -38,9 +45,9 @@ public function testWhenNoUserIsAvailable() $this->dm->flush(); $this->dm->clear(); - $repo = $this->dm->getRepository(self::ARTICLE); - $sport = $repo->findOneByTitle('sport no user'); - $this->assertEmpty($sport->getCreated()); - $this->assertEmpty($sport->getUpdated()); + $repo = $this->dm->getRepository(Article::class); + $sport = $repo->findOneBy(['title' => 'sport no user']); + static::assertEmpty($sport->getCreated()); + static::assertEmpty($sport->getUpdated()); } } diff --git a/tests/Gedmo/Blameable/ProtectedPropertySupperclassTest.php b/tests/Gedmo/Blameable/ProtectedPropertySupperclassTest.php index 0b1144e8f2..4473c0a0e5 100644 --- a/tests/Gedmo/Blameable/ProtectedPropertySupperclassTest.php +++ b/tests/Gedmo/Blameable/ProtectedPropertySupperclassTest.php @@ -1,25 +1,31 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; +use Gedmo\Blameable\BlameableListener; +use Gedmo\Tests\Blameable\Fixture\Entity\SupperClassExtension; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Translatable\Entity\Translation; use Gedmo\Translatable\TranslatableListener; -use Blameable\Fixture\Entity\SupperClassExtension; /** * These are tests for Blameable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ProtectedPropertySupperclassTest extends BaseTestCaseORM +final class ProtectedPropertySupperclassTest extends BaseTestCaseORM { - const SUPERCLASS = "Blameable\\Fixture\\Entity\\SupperClassExtension"; - const TRANSLATION = "Gedmo\\Translatable\\Entity\\Translation"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -31,10 +37,10 @@ protected function setUp() $blameableListener->setUserValue('testuser'); $evm->addEventSubscriber($blameableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testProtectedProperty() + public function testProtectedProperty(): void { $test = new SupperClassExtension(); $test->setName('name'); @@ -44,18 +50,18 @@ public function testProtectedProperty() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($test); - $this->assertCount(0, $translations); + static::assertCount(0, $translations); - $this->assertEquals('testuser', $test->getCreatedBy()); + static::assertSame('testuser', $test->getCreatedBy()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TRANSLATION, - self::SUPERCLASS, - ); + return [ + Translation::class, + SupperClassExtension::class, + ]; } } diff --git a/tests/Gedmo/Blameable/TraitUsageTest.php b/tests/Gedmo/Blameable/TraitUsageTest.php index 1bf2b1ddf0..f43a7ab3de 100644 --- a/tests/Gedmo/Blameable/TraitUsageTest.php +++ b/tests/Gedmo/Blameable/TraitUsageTest.php @@ -1,42 +1,41 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Blameable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Blameable\Fixture\Entity\UsingTrait; +use Gedmo\Blameable\BlameableListener; +use Gedmo\Tests\Blameable\Fixture\Entity\UsingTrait; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Blameable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TraitUsageTest extends BaseTestCaseORM +final class TraitUsageTest extends BaseTestCaseORM { - const TARGET = "Blameable\\Fixture\\Entity\\UsingTrait"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); - if (version_compare(PHP_VERSION, '5.4.0') < 0) { - $this->markTestSkipped('PHP >= 5.4 version required for this test.'); - } - $listener = new BlameableListener(); $listener->setUserValue('testuser'); $evm = new EventManager(); $evm->addEventSubscriber($listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldTimestampUsingTrait() + public function testShouldTimestampUsingTrait(): void { $sport = new UsingTrait(); $sport->setTitle('Sport'); @@ -44,24 +43,21 @@ public function shouldTimestampUsingTrait() $this->em->persist($sport); $this->em->flush(); - $this->assertNotNull($sport->getCreatedBy()); - $this->assertNotNull($sport->getUpdatedBy()); + static::assertNotNull($sport->getCreatedBy()); + static::assertNotNull($sport->getUpdatedBy()); } - /** - * @test - */ - public function traitMethodthShouldReturnObject() + public function testTraitMethodthShouldReturnObject(): void { $sport = new UsingTrait(); - $this->assertInstanceOf(self::TARGET, $sport->setCreatedBy('myuser')); - $this->assertInstanceOf(self::TARGET, $sport->setUpdatedBy('myuser')); + static::assertInstanceOf(UsingTrait::class, $sport->setCreatedBy('myuser')); + static::assertInstanceOf(UsingTrait::class, $sport->setUpdatedBy('myuser')); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TARGET, - ); + return [ + UsingTrait::class, + ]; } } diff --git a/tests/Gedmo/Clock.php b/tests/Gedmo/Clock.php new file mode 100644 index 0000000000..00b4473861 --- /dev/null +++ b/tests/Gedmo/Clock.php @@ -0,0 +1,22 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests; + +use Psr\Clock\ClockInterface; + +final class Clock implements ClockInterface +{ + public function now(): \DateTimeImmutable + { + return new \DateTimeImmutable(); + } +} diff --git a/tests/Gedmo/DoctrineExtensionsTest.php b/tests/Gedmo/DoctrineExtensionsTest.php new file mode 100644 index 0000000000..dcb91e19ff --- /dev/null +++ b/tests/Gedmo/DoctrineExtensionsTest.php @@ -0,0 +1,157 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\ODM\MongoDB\Mapping\Driver as DriverMongodbODM; +use Doctrine\ORM\Mapping\Driver as DriverORM; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\DoctrineExtensions; +use PHPUnit\Framework\TestCase; + +/** + * This test covers the driver registration helpers in the {@see DoctrineExtensions} class. + */ +final class DoctrineExtensionsTest extends TestCase +{ + /** + * @requires PHP >= 8.0 + */ + public function testRegistersAttributeDriverForConcreteOrmEntitiesToChain(): void + { + $chain = new MappingDriverChain(); + + DoctrineExtensions::registerMappingIntoDriverChainORM($chain); + + $drivers = $chain->getDrivers(); + + static::assertArrayHasKey('Gedmo', $drivers); + static::assertInstanceOf(DriverORM\AttributeDriver::class, $drivers['Gedmo'], 'The attribute driver should be registered to the chain on PHP 8'); + } + + public function testRegistersAnnotationDriverForConcreteOrmEntitiesToChain(): void + { + if (\PHP_VERSION_ID >= 80000 || !class_exists(AnnotationReader::class)) { + static::markTestSkipped('Test only applies to PHP 7 and requires the doctrine/annotations package'); + } + + $chain = new MappingDriverChain(); + + DoctrineExtensions::registerMappingIntoDriverChainORM($chain); + + $drivers = $chain->getDrivers(); + + static::assertArrayHasKey('Gedmo', $drivers); + static::assertInstanceOf(DriverORM\AnnotationDriver::class, $drivers['Gedmo'], 'The annotations driver should be registered to the chain on PHP 7'); + } + + /** + * @requires PHP >= 8.0 + */ + public function testRegistersAttributeDriverForAbstractOrmSuperclassesToChain(): void + { + $chain = new MappingDriverChain(); + + DoctrineExtensions::registerAbstractMappingIntoDriverChainORM($chain); + + $drivers = $chain->getDrivers(); + + static::assertArrayHasKey('Gedmo', $drivers); + static::assertInstanceOf(DriverORM\AttributeDriver::class, $drivers['Gedmo'], 'The attribute driver should be registered to the chain on PHP 8'); + } + + public function testRegistersAnnotationDriverForAbstractOrmSuperclassesToChain(): void + { + if (\PHP_VERSION_ID >= 80000 || !class_exists(AnnotationReader::class)) { + static::markTestSkipped('Test only applies to PHP 7 and requires the doctrine/annotations package'); + } + + $chain = new MappingDriverChain(); + + DoctrineExtensions::registerAbstractMappingIntoDriverChainORM($chain); + + $drivers = $chain->getDrivers(); + + static::assertArrayHasKey('Gedmo', $drivers); + static::assertInstanceOf(DriverORM\AnnotationDriver::class, $drivers['Gedmo'], 'The annotations driver should be registered to the chain on PHP 7'); + } + + /** + * @requires PHP >= 8.0 + */ + public function testRegistersAttributeDriverForConcreteOdmDocumentsToChain(): void + { + if (!class_exists(DriverMongodbODM\AttributeDriver::class)) { + static::markTestSkipped('Test requires the attribute mapping driver from the doctrine/mongodb-odm package'); + } + + $chain = new MappingDriverChain(); + + DoctrineExtensions::registerMappingIntoDriverChainMongodbODM($chain); + + $drivers = $chain->getDrivers(); + + static::assertArrayHasKey('Gedmo', $drivers); + static::assertInstanceOf(DriverMongodbODM\AttributeDriver::class, $drivers['Gedmo'], 'The attribute driver should be registered to the chain on PHP 8'); + } + + public function testRegistersAnnotationDriverForConcreteOdmDocumentsToChain(): void + { + if (\PHP_VERSION_ID >= 80000 || !class_exists(AnnotationReader::class)) { + static::markTestSkipped('Test only applies to PHP 7 and requires the doctrine/annotations package'); + } + + $chain = new MappingDriverChain(); + + DoctrineExtensions::registerMappingIntoDriverChainMongodbODM($chain); + + $drivers = $chain->getDrivers(); + + static::assertArrayHasKey('Gedmo', $drivers); + static::assertInstanceOf(DriverMongodbODM\AnnotationDriver::class, $drivers['Gedmo'], 'The annotations driver should be registered to the chain on PHP 7'); + } + + /** + * @requires PHP >= 8.0 + */ + public function testRegistersAttributeDriverForAbstractOdmSuperclassesToChain(): void + { + if (!class_exists(DriverMongodbODM\AttributeDriver::class)) { + static::markTestSkipped('Test requires the attribute mapping driver from the doctrine/mongodb-odm package'); + } + + $chain = new MappingDriverChain(); + + DoctrineExtensions::registerAbstractMappingIntoDriverChainMongodbODM($chain); + + $drivers = $chain->getDrivers(); + + static::assertArrayHasKey('Gedmo', $drivers); + static::assertInstanceOf(DriverMongodbODM\AttributeDriver::class, $drivers['Gedmo'], 'The attribute driver should be registered to the chain on PHP 8'); + } + + public function testRegistersAnnotationDriverForAbstractOdmSuperclassesToChain(): void + { + if (\PHP_VERSION_ID >= 80000 || !class_exists(AnnotationReader::class)) { + static::markTestSkipped('Test only applies to PHP 7 and requires the doctrine/annotations package'); + } + + $chain = new MappingDriverChain(); + + DoctrineExtensions::registerAbstractMappingIntoDriverChainMongodbODM($chain); + + $drivers = $chain->getDrivers(); + + static::assertArrayHasKey('Gedmo', $drivers); + static::assertInstanceOf(DriverMongodbODM\AnnotationDriver::class, $drivers['Gedmo'], 'The annotations driver should be registered to the chain on PHP 7'); + } +} diff --git a/tests/Gedmo/IpTraceable/ChangeTest.php b/tests/Gedmo/IpTraceable/ChangeTest.php index e952431cb1..d0dc6dc95f 100644 --- a/tests/Gedmo/IpTraceable/ChangeTest.php +++ b/tests/Gedmo/IpTraceable/ChangeTest.php @@ -1,26 +1,36 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use IpTraceable\Fixture\TitledArticle; +use Gedmo\IpTraceable\IpTraceableListener; +use Gedmo\Tests\IpTraceable\Fixture\TitledArticle; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for IpTraceable behavior * * @author Pierre-Charles Bertineau - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ChangeTest extends BaseTestCaseORM +final class ChangeTest extends BaseTestCaseORM { - const TEST_IP = '34.234.1.10'; - const FIXTURE = "IpTraceable\\Fixture\\TitledArticle"; + private const TEST_IP = '34.234.1.10'; + /** + * @var IpTraceableListener + */ protected $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -30,10 +40,10 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($this->listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testChange() + public function testChange(): void { $test = new TitledArticle(); $test->setTitle('Test'); @@ -43,29 +53,29 @@ public function testChange() $this->em->flush(); $this->em->clear(); - $test = $this->em->getRepository(self::FIXTURE)->findOneByTitle('Test'); + $test = $this->em->getRepository(TitledArticle::class)->findOneBy(['title' => 'Test']); $test->setTitle('New Title'); $this->em->persist($test); $this->em->flush(); $this->em->clear(); - //Changed - $this->assertEquals(self::TEST_IP, $test->getChtitle()); + // Changed + static::assertSame(self::TEST_IP, $test->getChtitle()); $this->listener->setIpValue('127.0.0.1'); - $test = $this->em->getRepository(self::FIXTURE)->findOneByTitle('New Title'); + $test = $this->em->getRepository(TitledArticle::class)->findOneBy(['title' => 'New Title']); $test->setText('New Text'); $this->em->persist($test); $this->em->flush(); $this->em->clear(); - //Not Changed - $this->assertEquals(self::TEST_IP, $test->getChtitle()); + // Not Changed + static::assertSame(self::TEST_IP, $test->getChtitle()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::FIXTURE, - ); + return [ + TitledArticle::class, + ]; } } diff --git a/tests/Gedmo/IpTraceable/Fixture/Article.php b/tests/Gedmo/IpTraceable/Fixture/Article.php index 3f42ab9b22..46797fe9a9 100644 --- a/tests/Gedmo/IpTraceable/Fixture/Article.php +++ b/tests/Gedmo/IpTraceable/Fixture/Article.php @@ -1,142 +1,172 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; use Gedmo\IpTraceable\IpTraceable; use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Article implements IpTraceable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** - * @ORM\OneToMany(targetEntity="IpTraceable\Fixture\Comment", mappedBy="article") + * @var Collection + * + * @ORM\OneToMany(targetEntity="Gedmo\Tests\IpTraceable\Fixture\Comment", mappedBy="article") */ + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article')] private $comments; /** - * @var string $created - * * @Gedmo\IpTraceable(on="create") + * * @ORM\Column(name="created", type="string", length=45) */ - private $created; + #[ORM\Column(name: 'created', type: Types::STRING, length: 45)] + #[Gedmo\IpTraceable(on: 'create')] + private ?string $created = null; /** - * @var string $updated - * * @ORM\Column(name="updated", type="string", length=45) + * * @Gedmo\IpTraceable */ - private $updated; + #[ORM\Column(name: 'updated', type: Types::STRING, length: 45)] + #[Gedmo\IpTraceable] + private ?string $updated = null; /** - * @var string $published - * * @ORM\Column(name="published", type="string", length=45, nullable=true) + * * @Gedmo\IpTraceable(on="change", field="type.title", value="Published") */ - private $published; + #[ORM\Column(name: 'published', type: Types::STRING, length: 45, nullable: true)] + #[Gedmo\IpTraceable(on: 'change', field: 'type.title', value: 'Published')] + private ?string $published = null; /** - * @var string $contentChanged - * * @ORM\Column(name="content_changed", type="string", length=45, nullable=true) + * * @Gedmo\IpTraceable(on="change", field={"title", "body"}) */ - private $contentChanged; + #[ORM\Column(name: 'content_changed', type: Types::STRING, length: 45, nullable: true)] + #[Gedmo\IpTraceable(on: 'change', field: ['title', 'body'])] + private ?string $contentChanged = null; /** * @ORM\ManyToOne(targetEntity="Type", inversedBy="articles") */ - private $type; + #[ORM\ManyToOne(targetEntity: Type::class, inversedBy: 'articles')] + private ?Type $type = null; - public function setType($type) + public function __construct() + { + $this->comments = new ArrayCollection(); + } + + public function setType(?Type $type): void { $this->type = $type; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function addComment(Comment $comment) + public function addComment(Comment $comment): void { $comment->setArticle($this); $this->comments[] = $comment; } - public function getComments() + /** + * @return Collection + */ + public function getComments(): Collection { return $this->comments; } - /** - * Get created - * - * @return string $created - */ - public function getCreated() + public function getCreated(): ?string { return $this->created; } - public function setCreated($created) + public function setCreated(?string $created): void { $this->created = $created; } - public function getPublished() + public function getPublished(): ?string { return $this->published; } - public function setPublished($published) + public function setPublished(?string $published): void { $this->published = $published; } - /** - * Get updated - * - * @return string $updated - */ - public function getUpdated() + public function getUpdated(): ?string { return $this->updated; } - public function setUpdated($updated) + public function setUpdated(?string $updated): void { $this->updated = $updated; } - public function setContentChanged($contentChanged) + public function setContentChanged(?string $contentChanged): void { $this->contentChanged = $contentChanged; } - public function getContentChanged() + public function getContentChanged(): ?string { return $this->contentChanged; } diff --git a/tests/Gedmo/IpTraceable/Fixture/Comment.php b/tests/Gedmo/IpTraceable/Fixture/Comment.php index 54e9bf49c0..50c032f3ba 100644 --- a/tests/Gedmo/IpTraceable/Fixture/Comment.php +++ b/tests/Gedmo/IpTraceable/Fixture/Comment.php @@ -1,86 +1,115 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; use Gedmo\IpTraceable\IpTraceable; use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Comment implements IpTraceable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="message", type="text") */ - private $message; + #[ORM\Column(name: 'message', type: Types::TEXT)] + private ?string $message = null; /** - * @ORM\ManyToOne(targetEntity="IpTraceable\Fixture\Article", inversedBy="comments") + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\IpTraceable\Fixture\Article", inversedBy="comments") */ - private $article; + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'comments')] + private ?Article $article = null; /** * @ORM\Column(type="integer") */ - private $status; + #[ORM\Column(type: Types::INTEGER)] + private ?int $status = null; /** - * @var string $closed + * @var string|null * * @ORM\Column(name="closed", type="string", length=45, nullable=true) + * * @Gedmo\IpTraceable(on="change", field="status", value=1) */ + #[ORM\Column(name: 'closed', type: Types::STRING, length: 45, nullable: true)] + #[Gedmo\IpTraceable(on: 'change', field: 'status', value: 1)] private $closed; /** - * @var string $modified + * @var string|null * * @ORM\Column(name="modified", type="string", length=45) + * * @Gedmo\IpTraceable(on="update") */ + #[ORM\Column(name: 'modified', type: Types::STRING, length: 45)] + #[Gedmo\IpTraceable(on: 'update')] private $modified; - public function setArticle($article) + public function setArticle(?Article $article): void { $this->article = $article; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setStatus($status) + public function setStatus(?int $status): void { $this->status = $status; } - public function getStatus() + public function getStatus(): ?int { return $this->status; } - public function setMessage($message) + public function setMessage(?string $message): void { $this->message = $message; } - public function getMessage() + public function getMessage(): ?string { return $this->message; } - public function getModified() + public function getModified(): ?string { return $this->modified; } - public function getClosed() + public function getClosed(): ?string { return $this->closed; } diff --git a/tests/Gedmo/IpTraceable/Fixture/Document/Article.php b/tests/Gedmo/IpTraceable/Fixture/Document/Article.php index bd130d348a..a9f73dc0da 100644 --- a/tests/Gedmo/IpTraceable/Fixture/Document/Article.php +++ b/tests/Gedmo/IpTraceable/Fixture/Document/Article.php @@ -1,140 +1,163 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { - /** @ODM\Id */ + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceOne(targetDocument="Type") + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\IpTraceable\Fixture\Document\Type") */ - private $type; + #[ODM\ReferenceOne(targetDocument: Type::class)] + private ?Type $type = null; /** - * @var string $created + * @ODM\Field(type="string") * - * @ODM\String * @Gedmo\IpTraceable(on="create") */ - private $created; + #[ODM\Field(type: MongoDBType::STRING)] + #[Gedmo\IpTraceable(on: 'create')] + private ?string $created = null; /** - * @var string $updated + * @ODM\Field(type="string") * - * @ODM\String * @Gedmo\IpTraceable */ - private $updated; + #[ODM\Field(type: MongoDBType::STRING)] + #[Gedmo\IpTraceable] + private ?string $updated = null; /** - * @var string $published + * @ODM\Field(type="string") * - * @ODM\String * @Gedmo\IpTraceable(on="change", field="type.title", value="Published") */ - private $published; + #[ODM\Field(type: MongoDBType::STRING)] + #[Gedmo\IpTraceable(on: 'change', field: 'type.title', value: 'Published')] + private ?string $published = null; /** - * @var string - * @ODM\String + * @ODM\Field(type="string") + * * @Gedmo\IpTraceable(on="change", field="isReady", value=true) */ - private $ready; + #[ODM\Field(type: MongoDBType::STRING)] + #[Gedmo\IpTraceable(on: 'change', field: 'isReady', value: true)] + private ?string $ready = null; /** - * @var bool - * @ODM\Boolean + * @ODM\Field(type="bool") */ - private $isReady = false; + #[ODM\Field(type: MongoDBType::BOOL)] + private bool $isReady = false; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getCreated() + public function getCreated(): ?string { return $this->created; } - public function getPublished() + public function getPublished(): ?string { return $this->published; } - public function getUpdated() + public function getUpdated(): ?string { return $this->updated; } - public function setType(Type $type) + public function setType(Type $type): void { $this->type = $type; } - public function getType() + public function getType(): ?Type { return $this->type; } - public function setCreated($created) + public function setCreated(?string $created): void { $this->created = $created; } - public function setPublished($published) + public function setPublished(?string $published): void { $this->published = $published; } - public function setUpdated($updated) + public function setUpdated(?string $updated): void { $this->updated = $updated; } - public function setReady($ready) + public function setReady(?string $ready): self { $this->ready = $ready; return $this; } - public function getReady() + public function getReady(): ?string { return $this->ready; } - public function setIsReady($isReady) + public function setIsReady(bool $isReady): self { $this->isReady = $isReady; return $this; } - public function getIsReady() + public function getIsReady(): bool { return $this->isReady; } diff --git a/tests/Gedmo/IpTraceable/Fixture/Document/Type.php b/tests/Gedmo/IpTraceable/Fixture/Document/Type.php index 5fa2e04de8..4f445fa0b5 100644 --- a/tests/Gedmo/IpTraceable/Fixture/Document/Type.php +++ b/tests/Gedmo/IpTraceable/Fixture/Document/Type.php @@ -1,48 +1,66 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="types") */ +#[ODM\Document(collection: 'types')] class Type { - /** @ODM\Id */ + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $identifier; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $identifier = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getIdentifier() + public function getIdentifier(): ?string { return $this->identifier; } - public function setIdentifier($identifier) + public function setIdentifier(?string $identifier): void { $this->identifier = $identifier; } diff --git a/tests/Gedmo/IpTraceable/Fixture/MappedSupperClass.php b/tests/Gedmo/IpTraceable/Fixture/MappedSupperClass.php index 39d4d4076f..17353d90a3 100644 --- a/tests/Gedmo/IpTraceable/Fixture/MappedSupperClass.php +++ b/tests/Gedmo/IpTraceable/Fixture/MappedSupperClass.php @@ -1,84 +1,87 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** -* @ORM\MappedSuperclass -*/ + * @ORM\MappedSuperclass + */ +#[ORM\MappedSuperclass] class MappedSupperClass { /** - * @var integer $id - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - */ + * @var int|null + * + * @ORM\Column(name="id", type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] protected $id; /** - * @var string $locale - * - * @Gedmo\Locale - */ + * @var string|null + * + * @Gedmo\Locale + */ + #[Gedmo\Locale] protected $locale; /** - * @var string $title - * - * @Gedmo\Translatable - * @ORM\Column(name="name", type="string", length=255) - */ + * @var string|null + * + * @Gedmo\Translatable + * + * @ORM\Column(name="name", type="string", length=191) + */ + #[Gedmo\Translatable] + #[ORM\Column(name: 'name', type: Types::STRING, length: 191)] protected $name; /** - * @var string $createdAt - * - * @ORM\Column(name="created_at", type="string", length=45) - * @Gedmo\IpTraceable(on="create") - */ + * @var string|null + * + * @ORM\Column(name="created_at", type="string", length=45) + * + * @Gedmo\IpTraceable(on="create") + */ + #[ORM\Column(name: 'created_at', type: Types::STRING, length: 45)] + #[Gedmo\IpTraceable(on: 'create')] protected $createdFromIp; /** - * Get id - * - * @return integer $id - * @codeCoverageIgnore - */ - public function getId() + * @codeCoverageIgnore + */ + public function getId(): ?int { return $this->id; } - /** - * Set name - * - * @param string $name - */ - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - /** - * Get name - * - * @return string $name - */ - public function getName() + public function getName(): ?string { return $this->name; } - /** - * Get createdFromIp - * - * @return string $createdFromIp - */ - public function getCreatedFromIp() + public function getCreatedFromIp(): ?string { return $this->createdFromIp; } diff --git a/tests/Gedmo/IpTraceable/Fixture/SupperClassExtension.php b/tests/Gedmo/IpTraceable/Fixture/SupperClassExtension.php index 33aeff2694..a38597045c 100644 --- a/tests/Gedmo/IpTraceable/Fixture/SupperClassExtension.php +++ b/tests/Gedmo/IpTraceable/Fixture/SupperClassExtension.php @@ -1,27 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable\Fixture; -use Gedmo\Mapping\Annotation as Gedmo; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class SupperClassExtension extends MappedSupperClass { /** * @ORM\Column(length=128) + * * @Gedmo\Translatable */ - private $title; + #[ORM\Column(length: 128)] + #[Gedmo\Translatable] + private ?string $title = null; - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/IpTraceable/Fixture/TitledArticle.php b/tests/Gedmo/IpTraceable/Fixture/TitledArticle.php index 55d5e8dca7..3447a24022 100644 --- a/tests/Gedmo/IpTraceable/Fixture/TitledArticle.php +++ b/tests/Gedmo/IpTraceable/Fixture/TitledArticle.php @@ -1,13 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; use Gedmo\IpTraceable\IpTraceable; use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class TitledArticle implements IpTraceable { /** @@ -15,92 +27,87 @@ class TitledArticle implements IpTraceable * @ORM\GeneratedValue * @ORM\Column(type="integer") */ - private $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; /** * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** * @ORM\Column(name="text", type="string", length=128) */ - private $text; + #[ORM\Column(name: 'text', type: Types::STRING, length: 128)] + private ?string $text = null; /** - * @var string $updated - * * @ORM\Column(name="chtext", type="string", length=45, nullable=true) + * * @Gedmo\IpTraceable(on="change", field="text") */ - private $chtext; + #[ORM\Column(name: 'chtext', type: Types::STRING, length: 45, nullable: true)] + #[Gedmo\IpTraceable(on: 'change', field: 'text')] + private ?string $chtext = null; /** - * @var string $chtitle - * * @ORM\Column(name="chtitle", type="string", length=45, nullable=true) + * * @Gedmo\IpTraceable(on="change", field="title") */ - private $chtitle; + #[ORM\Column(name: 'chtitle', type: Types::STRING, length: 45, nullable: true)] + #[Gedmo\IpTraceable(on: 'change', field: 'title')] + private ?string $chtitle = null; - /** - * @param string $chtext - */ - public function setChtext($chtext) + public function setChtext(?string $chtext): void { $this->chtext = $chtext; } - /** - * @return string - */ - public function getChtext() + public function getChtext(): ?string { return $this->chtext; } - /** - * @param string $chtitle - */ - public function setChtitle($chtitle) + public function setChtitle(?string $chtitle): void { $this->chtitle = $chtitle; } - /** - * @return string - */ - public function getChtitle() + public function getChtitle(): ?string { return $this->chtitle; } - public function setId($id) + public function setId(?int $id): void { $this->id = $id; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setText($text) + public function setText(?string $text): void { $this->text = $text; } - public function getText() + public function getText(): ?string { return $this->text; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/IpTraceable/Fixture/Type.php b/tests/Gedmo/IpTraceable/Fixture/Type.php index fa581ef0dd..51e12e04b9 100644 --- a/tests/Gedmo/IpTraceable/Fixture/Type.php +++ b/tests/Gedmo/IpTraceable/Fixture/Type.php @@ -1,37 +1,69 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Type { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="type") */ - private $articles; + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'type')] + private Collection $articles; + + public function __construct() + { + $this->articles = new ArrayCollection(); + } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/IpTraceable/Fixture/UsingTrait.php b/tests/Gedmo/IpTraceable/Fixture/UsingTrait.php index 0067754ff8..5d4f891b56 100644 --- a/tests/Gedmo/IpTraceable/Fixture/UsingTrait.php +++ b/tests/Gedmo/IpTraceable/Fixture/UsingTrait.php @@ -1,43 +1,61 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\IpTraceable\Traits\IpTraceableEntity; /** * @ORM\Entity */ +#[ORM\Entity] class UsingTrait { - /** + /* * Hook ipTraceable behavior * updates createdFromIp, updatedFromIp fields */ use IpTraceableEntity; /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(length: 128)] + private ?string $title = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/IpTraceable/Fixture/WithoutInterface.php b/tests/Gedmo/IpTraceable/Fixture/WithoutInterface.php index 0f815d1ef5..f83c2f3e25 100644 --- a/tests/Gedmo/IpTraceable/Fixture/WithoutInterface.php +++ b/tests/Gedmo/IpTraceable/Fixture/WithoutInterface.php @@ -1,56 +1,87 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class WithoutInterface { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(type="string", length=128) */ - private $title; + #[ORM\Column(type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\IpTraceable(on="create") + * * @ORM\Column(type="string", length=45) */ + #[ORM\Column(type: Types::STRING, length: 45)] + #[Gedmo\IpTraceable(on: 'create')] private $created; /** + * @var string|null + * * @ORM\Column(type="string", length=45) + * * @Gedmo\IpTraceable(on="update") */ + #[ORM\Column(type: Types::STRING, length: 45)] + #[Gedmo\IpTraceable(on: 'update')] private $updated; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getCreated() + public function getCreated(): ?string { return $this->created; } - public function getUpdated() + public function getUpdated(): ?string { return $this->updated; } diff --git a/tests/Gedmo/IpTraceable/IpTraceableDocumentTest.php b/tests/Gedmo/IpTraceable/IpTraceableDocumentTest.php index 564d5e3087..b2b232fa3e 100644 --- a/tests/Gedmo/IpTraceable/IpTraceableDocumentTest.php +++ b/tests/Gedmo/IpTraceable/IpTraceableDocumentTest.php @@ -1,27 +1,32 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use IpTraceable\Fixture\Document\Article; -use IpTraceable\Fixture\Document\Type; +use Gedmo\IpTraceable\IpTraceableListener; +use Gedmo\Tests\IpTraceable\Fixture\Document\Article; +use Gedmo\Tests\IpTraceable\Fixture\Document\Type; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for IpTraceable behavior ODM implementation * * @author Pierre-Charles Bertineau - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class IpTraceableDocumentTest extends BaseTestCaseMongoODM +final class IpTraceableDocumentTest extends BaseTestCaseMongoODM { - const TEST_IP = '34.234.1.10'; - - const ARTICLE = 'IpTraceable\Fixture\Document\Article'; - const TYPE = 'IpTraceable\Fixture\Document\Type'; + private const TEST_IP = '34.234.1.10'; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -31,17 +36,17 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($listener); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); $this->populate(); } - public function testIpTraceable() + public function testIpTraceable(): void { - $repo = $this->dm->getRepository(self::ARTICLE); - $article = $repo->findOneByTitle('IpTraceable Article'); + $repo = $this->dm->getRepository(Article::class); + $article = $repo->findOneBy(['title' => 'IpTraceable Article']); - $this->assertEquals(self::TEST_IP, $article->getCreated()); - $this->assertEquals(self::TEST_IP, $article->getUpdated()); + static::assertSame(self::TEST_IP, $article->getCreated()); + static::assertSame(self::TEST_IP, $article->getUpdated()); $published = new Type(); $published->setIdentifier('published'); @@ -53,13 +58,13 @@ public function testIpTraceable() $this->dm->flush(); $this->dm->clear(); - $article = $repo->findOneByTitle('IpTraceable Article'); + $article = $repo->findOneBy(['title' => 'IpTraceable Article']); - $this->assertEquals(self::TEST_IP, $article->getPublished()); - $this->assertEquals(self::TEST_IP, $article->getCreated()); + static::assertSame(self::TEST_IP, $article->getPublished()); + static::assertSame(self::TEST_IP, $article->getCreated()); } - public function testForcedValues() + public function testForcedValues(): void { $sport = new Article(); $sport->setTitle('sport forced'); @@ -70,10 +75,10 @@ public function testForcedValues() $this->dm->flush(); $this->dm->clear(); - $repo = $this->dm->getRepository(self::ARTICLE); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals(self::TEST_IP, (string) $sport->getCreated()); - $this->assertEquals(self::TEST_IP, $sport->getUpdated()); + $repo = $this->dm->getRepository(Article::class); + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame(self::TEST_IP, (string) $sport->getCreated()); + static::assertSame(self::TEST_IP, $sport->getUpdated()); $published = new Type(); $published->setIdentifier('published'); @@ -86,11 +91,11 @@ public function testForcedValues() $this->dm->flush(); $this->dm->clear(); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals(self::TEST_IP, $sport->getPublished()); + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame(self::TEST_IP, $sport->getPublished()); } - private function populate() + private function populate(): void { $art0 = new Article(); $art0->setTitle('IpTraceable Article'); diff --git a/tests/Gedmo/IpTraceable/IpTraceableTest.php b/tests/Gedmo/IpTraceable/IpTraceableTest.php index 58a417177a..a317aa9d25 100644 --- a/tests/Gedmo/IpTraceable/IpTraceableTest.php +++ b/tests/Gedmo/IpTraceable/IpTraceableTest.php @@ -1,91 +1,150 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use IpTraceable\Fixture\Article; -use IpTraceable\Fixture\Comment; -use IpTraceable\Fixture\Type; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\IpTraceable\IpTraceableListener; +use Gedmo\IpTraceable\Mapping\Event\IpTraceableAdapter; +use Gedmo\Tests\IpTraceable\Fixture\Article; +use Gedmo\Tests\IpTraceable\Fixture\Comment; +use Gedmo\Tests\IpTraceable\Fixture\Type; +use Gedmo\Tests\TestIpAddressProvider; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for IpTraceable behavior * * @author Pierre-Charles Bertineau - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class IpTraceableTest extends BaseTestCaseORM +final class IpTraceableTest extends BaseTestCaseORM { - const TEST_IP = '34.234.1.10'; + private const TEST_IP = '34.234.1.10'; + private const TEST_PROVIDER_IP = '34.234.2.10'; - const ARTICLE = "IpTraceable\\Fixture\\Article"; - const COMMENT = "IpTraceable\\Fixture\\Comment"; - const TYPE = "IpTraceable\\Fixture\\Type"; + private IpTraceableListener $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); - $listener = new IpTraceableListener(); - $listener->setIpValue(self::TEST_IP); + $this->listener = new IpTraceableListener(); + $this->listener->setIpValue(self::TEST_IP); $evm = new EventManager(); - $evm->addEventSubscriber($listener); + $evm->addEventSubscriber($this->listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testInvalidIpShouldThrowInvalidArgumentException() + public function testInvalidIpShouldThrowInvalidArgumentException(): void { $listener = new IpTraceableListener(); - $this->setExpectedException('Gedmo\Exception\InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $listener->setIpValue('xx.xxx.xx.xxx'); } - public function testIpV4() + public function testIpV4(): void { $listener = new IpTraceableListener(); $listener->setIpValue('123.218.45.39'); - $this->assertEquals('123.218.45.39', $listener->getFieldValue(null, null, null)); + static::assertSame('123.218.45.39', $listener->getFieldValue( + static::createStub(ClassMetadata::class), + 'ip', + static::createStub(IpTraceableAdapter::class) + )); } - public function testIpV6() + public function testIpV6(): void { $listener = new IpTraceableListener(); $listener->setIpValue('2001:0db8:0000:85a3:0000:0000:ac1f:8001'); - $this->assertEquals('2001:0db8:0000:85a3:0000:0000:ac1f:8001', $listener->getFieldValue(null, null, null)); + static::assertSame('2001:0db8:0000:85a3:0000:0000:ac1f:8001', $listener->getFieldValue( + static::createStub(ClassMetadata::class), + 'ip', + static::createStub(IpTraceableAdapter::class) + )); } - public function testIpTraceable() + public function testIpTraceable(): void { $sport = new Article(); $sport->setTitle('Sport'); - $this->assertTrue($sport instanceof IpTraceable); - $sportComment = new Comment(); $sportComment->setMessage('hello'); $sportComment->setArticle($sport); $sportComment->setStatus(0); - $this->assertTrue($sportComment instanceof IpTraceable); + $this->em->persist($sport); + $this->em->persist($sportComment); + $this->em->flush(); + $this->em->clear(); + + $sport = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); + static::assertSame(self::TEST_IP, $sport->getCreated()); + static::assertSame(self::TEST_IP, $sport->getUpdated()); + static::assertNull($sport->getPublished()); + + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertSame(self::TEST_IP, $sportComment->getModified()); + static::assertNull($sportComment->getClosed()); + + $sportComment->setStatus(1); + $published = new Type(); + $published->setTitle('Published'); + + $sport->setTitle('Updated'); + $sport->setType($published); + $this->em->persist($sport); + $this->em->persist($published); + $this->em->persist($sportComment); + $this->em->flush(); + $this->em->clear(); + + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertSame(self::TEST_IP, $sportComment->getClosed()); + + static::assertSame(self::TEST_IP, $sport->getPublished()); + } + + public function testIpTraceableWithProvider(): void + { + $this->listener->setIpAddressProvider(new TestIpAddressProvider(self::TEST_PROVIDER_IP)); + + $sport = new Article(); + $sport->setTitle('Sport'); + + $sportComment = new Comment(); + $sportComment->setMessage('hello'); + $sportComment->setArticle($sport); + $sportComment->setStatus(0); $this->em->persist($sport); $this->em->persist($sportComment); $this->em->flush(); $this->em->clear(); - $sport = $this->em->getRepository(self::ARTICLE)->findOneByTitle('Sport'); - $this->assertEquals(self::TEST_IP, $sport->getCreated()); - $this->assertEquals(self::TEST_IP, $sport->getUpdated()); - $this->assertNull($sport->getPublished()); + $sport = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); + static::assertSame(self::TEST_PROVIDER_IP, $sport->getCreated()); + static::assertSame(self::TEST_PROVIDER_IP, $sport->getUpdated()); + static::assertNull($sport->getPublished()); - $sportComment = $this->em->getRepository(self::COMMENT)->findOneByMessage('hello'); - $this->assertEquals(self::TEST_IP, $sportComment->getModified()); - $this->assertNull($sportComment->getClosed()); + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertSame(self::TEST_PROVIDER_IP, $sportComment->getModified()); + static::assertNull($sportComment->getClosed()); $sportComment->setStatus(1); $published = new Type(); @@ -99,13 +158,13 @@ public function testIpTraceable() $this->em->flush(); $this->em->clear(); - $sportComment = $this->em->getRepository(self::COMMENT)->findOneByMessage('hello'); - $this->assertEquals(self::TEST_IP, $sportComment->getClosed()); + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertSame(self::TEST_PROVIDER_IP, $sportComment->getClosed()); - $this->assertEquals(self::TEST_IP, $sport->getPublished()); + static::assertSame(self::TEST_PROVIDER_IP, $sport->getPublished()); } - public function testForcedValues() + public function testForcedValues(): void { $sport = new Article(); $sport->setTitle('sport forced'); @@ -116,10 +175,10 @@ public function testForcedValues() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::ARTICLE); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals(self::TEST_IP, $sport->getCreated()); - $this->assertEquals(self::TEST_IP, $sport->getUpdated()); + $repo = $this->em->getRepository(Article::class); + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame(self::TEST_IP, $sport->getCreated()); + static::assertSame(self::TEST_IP, $sport->getUpdated()); $published = new Type(); $published->setTitle('Published'); @@ -131,16 +190,16 @@ public function testForcedValues() $this->em->flush(); $this->em->clear(); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals(self::TEST_IP, $sport->getPublished()); + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame(self::TEST_IP, $sport->getPublished()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::COMMENT, - self::TYPE, - ); + return [ + Article::class, + Comment::class, + Type::class, + ]; } } diff --git a/tests/Gedmo/IpTraceable/NoInterfaceTest.php b/tests/Gedmo/IpTraceable/NoInterfaceTest.php index 0ffab5ea0a..e23b9d540d 100644 --- a/tests/Gedmo/IpTraceable/NoInterfaceTest.php +++ b/tests/Gedmo/IpTraceable/NoInterfaceTest.php @@ -1,24 +1,31 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use IpTraceable\Fixture\WithoutInterface; +use Gedmo\IpTraceable\IpTraceableListener; +use Gedmo\Tests\IpTraceable\Fixture\WithoutInterface; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for IpTraceable behavior * * @author Pierre-Charles Bertineau - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class NoInterfaceTest extends BaseTestCaseORM +final class NoInterfaceTest extends BaseTestCaseORM { - const TEST_IP = '34.234.1.10'; - const FIXTURE = "IpTraceable\\Fixture\\WithoutInterface"; + private const TEST_IP = '34.234.1.10'; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -27,10 +34,10 @@ protected function setUp() $ipTraceableListener->setIpValue(self::TEST_IP); $evm->addEventSubscriber($ipTraceableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testIpTraceableNoInterface() + public function testIpTraceableNoInterface(): void { $test = new WithoutInterface(); $test->setTitle('Test'); @@ -39,15 +46,15 @@ public function testIpTraceableNoInterface() $this->em->flush(); $this->em->clear(); - $test = $this->em->getRepository(self::FIXTURE)->findOneByTitle('Test'); - $this->assertEquals(self::TEST_IP, $test->getCreated()); - $this->assertEquals(self::TEST_IP, $test->getUpdated()); + $test = $this->em->getRepository(WithoutInterface::class)->findOneBy(['title' => 'Test']); + static::assertSame(self::TEST_IP, $test->getCreated()); + static::assertSame(self::TEST_IP, $test->getUpdated()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::FIXTURE, - ); + return [ + WithoutInterface::class, + ]; } } diff --git a/tests/Gedmo/IpTraceable/TraitUsageTest.php b/tests/Gedmo/IpTraceable/TraitUsageTest.php index 61f7f4154d..6c31d5eded 100644 --- a/tests/Gedmo/IpTraceable/TraitUsageTest.php +++ b/tests/Gedmo/IpTraceable/TraitUsageTest.php @@ -1,43 +1,43 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\IpTraceable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use IpTraceable\Fixture\UsingTrait; +use Gedmo\IpTraceable\IpTraceableListener; +use Gedmo\Tests\IpTraceable\Fixture\UsingTrait; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for IpTraceable behavior * * @author Pierre-Charles Bertineau - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TraitUsageTest extends BaseTestCaseORM +final class TraitUsageTest extends BaseTestCaseORM { - const TEST_IP = '34.234.1.10'; - const TARGET = "IpTraceable\\Fixture\\UsingTrait"; + private const TEST_IP = '34.234.1.10'; - protected function setUp() + protected function setUp(): void { parent::setUp(); - if (version_compare(PHP_VERSION, '5.4.0') < 0) { - $this->markTestSkipped('PHP >= 5.4 version required for this test.'); - } - $evm = new EventManager(); $ipTraceableListener = new IpTraceableListener(); $ipTraceableListener->setIpValue(self::TEST_IP); $evm->addEventSubscriber($ipTraceableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldIpTraceUsingTrait() + public function testShouldIpTraceUsingTrait(): void { $sport = new UsingTrait(); $sport->setTitle('Sport'); @@ -45,24 +45,21 @@ public function shouldIpTraceUsingTrait() $this->em->persist($sport); $this->em->flush(); - $this->assertNotNull($sport->getCreatedFromIp()); - $this->assertNotNull($sport->getUpdatedFromIp()); + static::assertNotNull($sport->getCreatedFromIp()); + static::assertNotNull($sport->getUpdatedFromIp()); } - /** - * @test - */ - public function traitMethodShouldReturnObject() + public function testTraitMethodShouldReturnObject(): void { $sport = new UsingTrait(); - $this->assertInstanceOf('IpTraceable\Fixture\UsingTrait', $sport->setCreatedFromIp('<192 class="158 3 43">')); - $this->assertInstanceOf('IpTraceable\Fixture\UsingTrait', $sport->setUpdatedFromIp('<192 class="158 3 43">')); + static::assertInstanceOf(UsingTrait::class, $sport->setCreatedFromIp('<192 class="158 3 43">')); + static::assertInstanceOf(UsingTrait::class, $sport->setUpdatedFromIp('<192 class="158 3 43">')); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TARGET, - ); + return [ + UsingTrait::class, + ]; } } diff --git a/tests/Gedmo/Loggable/AnnotationLoggableEntityTest.php b/tests/Gedmo/Loggable/AnnotationLoggableEntityTest.php new file mode 100644 index 0000000000..8d4a5cdb98 --- /dev/null +++ b/tests/Gedmo/Loggable/AnnotationLoggableEntityTest.php @@ -0,0 +1,35 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Loggable\LoggableEntityTest; + +/** + * These are tests for loggable behavior with an annotation reader (created by the listener by default) + * + * @author Gediminas Morkevicius + */ +final class AnnotationLoggableEntityTest extends LoggableEntityTest +{ + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $this->listener = new LoggableListener(); + $this->listener->setUsername('jules'); + $evm->addEventSubscriber($this->listener); + + $this->em = $this->getDefaultMockSqliteEntityManager($evm); + } +} diff --git a/tests/Gedmo/Loggable/AttributeLoggableEntityTest.php b/tests/Gedmo/Loggable/AttributeLoggableEntityTest.php new file mode 100644 index 0000000000..23e6828b6a --- /dev/null +++ b/tests/Gedmo/Loggable/AttributeLoggableEntityTest.php @@ -0,0 +1,39 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Loggable; + +use Doctrine\Common\EventManager; +use Gedmo\Mapping\Driver\AttributeReader; +use Gedmo\Tests\Loggable\LoggableEntityTest; + +/** + * These are tests for loggable behavior with an attribute reader + * + * @requires PHP >= 8.0 + * + * @author Gediminas Morkevicius + */ +final class AttributeLoggableEntityTest extends LoggableEntityTest +{ + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $this->listener = new LoggableListener(); + $this->listener->setAnnotationReader(new AttributeReader()); + $this->listener->setUsername('jules'); + $evm->addEventSubscriber($this->listener); + + $this->em = $this->getDefaultMockSqliteEntityManager($evm); + } +} diff --git a/tests/Gedmo/Loggable/Fixture/Document/Article.php b/tests/Gedmo/Loggable/Fixture/Document/Article.php index 4fe0230af6..2ff55057a1 100644 --- a/tests/Gedmo/Loggable/Fixture/Document/Article.php +++ b/tests/Gedmo/Loggable/Fixture/Document/Article.php @@ -1,57 +1,82 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; +use Gedmo\Loggable\Loggable; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="articles") + * * @Gedmo\Loggable */ -class Article +#[ODM\Document(collection: 'articles')] +#[Gedmo\Loggable] +class Article implements Loggable { - /** @ODM\Id */ + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] private $id; /** * @Gedmo\Versioned - * @ODM\String + * + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: Type::STRING)] + #[Gedmo\Versioned] + private ?string $title = null; /** - * @ODM\EmbedOne(targetDocument="Author") + * @ODM\EmbedOne(targetDocument="Gedmo\Tests\Loggable\Fixture\Document\Author") + * * @Gedmo\Versioned */ - private $author; + #[ODM\EmbedOne(targetDocument: Author::class)] + #[Gedmo\Versioned] + private ?Author $author = null; public function __toString() { return $this->title; } - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setAuthor($author) + public function setAuthor(?Author $author): void { $this->author = $author; } - public function getAuthor() + public function getAuthor(): ?Author { return $this->author; } diff --git a/tests/Gedmo/Loggable/Fixture/Document/Author.php b/tests/Gedmo/Loggable/Fixture/Document/Author.php index 0076db07bb..b6010d47d3 100644 --- a/tests/Gedmo/Loggable/Fixture/Document/Author.php +++ b/tests/Gedmo/Loggable/Fixture/Document/Author.php @@ -1,49 +1,69 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; +use Gedmo\Loggable\Loggable; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\EmbeddedDocument + * * @Gedmo\Loggable */ -class Author +#[ODM\EmbeddedDocument] +#[Gedmo\Loggable] +class Author implements Loggable { /** * @Gedmo\Versioned - * @ODM\String + * + * @ODM\Field(type="string") */ - private $name; + #[ODM\Field(type: Type::STRING)] + #[Gedmo\Versioned] + private ?string $name = null; /** * @Gedmo\Versioned - * @ODM\String + * + * @ODM\Field(type="string") */ - private $email; + #[ODM\Field(type: Type::STRING)] + #[Gedmo\Versioned] + private ?string $email = null; public function __toString() { - return $this->getName(); + return (string) $this->getName(); } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setEmail($email) + public function setEmail(?string $email): void { $this->email = $email; } - public function getEmail() + public function getEmail(): ?string { return $this->email; } diff --git a/tests/Gedmo/Loggable/Fixture/Document/Comment.php b/tests/Gedmo/Loggable/Fixture/Document/Comment.php index 417a2fede1..939d31972a 100644 --- a/tests/Gedmo/Loggable/Fixture/Document/Comment.php +++ b/tests/Gedmo/Loggable/Fixture/Document/Comment.php @@ -1,86 +1,116 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; +use Gedmo\Loggable\Loggable; use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tests\Loggable\Fixture\Document\Log\Comment as CommentLog; /** * @ODM\Document - * @Gedmo\Loggable(logEntryClass="Loggable\Fixture\Document\Log\Comment") + * + * @Gedmo\Loggable(logEntryClass="Gedmo\Tests\Loggable\Fixture\Document\Log\Comment") */ -class Comment +#[ODM\Document] +#[Gedmo\Loggable(logEntryClass: CommentLog::class)] +class Comment implements Loggable { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** * @Gedmo\Versioned - * @ODM\String + * + * @ODM\Field(type="string") */ - private $subject; + #[ODM\Field(type: Type::STRING)] + #[Gedmo\Versioned] + private ?string $subject = null; /** * @Gedmo\Versioned - * @ODM\String + * + * @ODM\Field(type="string") */ - private $message; + #[ODM\Field(type: Type::STRING)] + #[Gedmo\Versioned] + private ?string $message = null; /** * @Gedmo\Versioned - * @ODM\ReferenceOne(targetDocument="RelatedArticle", inversedBy="comments") + * + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\Loggable\Fixture\Document\RelatedArticle", inversedBy="comments") */ - private $article; + #[ODM\ReferenceOne(targetDocument: RelatedArticle::class, inversedBy: 'comments')] + #[Gedmo\Versioned] + private ?RelatedArticle $article = null; /** - * @ODM\EmbedOne(targetDocument="Author") + * @ODM\EmbedOne(targetDocument="Gedmo\Tests\Loggable\Fixture\Document\Author") + * * @Gedmo\Versioned */ - private $author; + #[ODM\EmbedOne(targetDocument: Author::class)] + #[Gedmo\Versioned] + private ?Author $author = null; - public function setArticle($article) + public function setArticle(?RelatedArticle $article): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?RelatedArticle { return $this->article; } - public function getId() + public function getId(): ?string { return $this->id; } - public function setSubject($subject) + public function setSubject(?string $subject): void { $this->subject = $subject; } - public function getSubject() + public function getSubject(): ?string { return $this->subject; } - public function setMessage($message) + public function setMessage(?string $message): void { $this->message = $message; } - public function getMessage() + public function getMessage(): ?string { return $this->message; } - public function setAuthor($author) + public function setAuthor(?Author $author): void { $this->author = $author; } - public function getAuthor() + public function getAuthor(): ?Author { return $this->author; } diff --git a/tests/Gedmo/Loggable/Fixture/Document/Log/Comment.php b/tests/Gedmo/Loggable/Fixture/Document/Log/Comment.php index ee3d15b918..61940d4023 100644 --- a/tests/Gedmo/Loggable/Fixture/Document/Log/Comment.php +++ b/tests/Gedmo/Loggable/Fixture/Document/Log/Comment.php @@ -1,16 +1,30 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Document\Log; -use Gedmo\Loggable\Document\MappedSuperclass\AbstractLogEntry; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Gedmo\Loggable\Document\MappedSuperclass\AbstractLogEntry; +use Gedmo\Loggable\Document\Repository\LogEntryRepository; +use Gedmo\Tests\Loggable\Fixture\Document\Comment as CommentDocument; /** * @ODM\Document( * collection="test_comment_log_entries", * repositoryClass="Gedmo\Loggable\Document\Repository\LogEntryRepository" * ) + * + * @phpstan-extends AbstractLogEntry */ +#[ODM\Document(collection: 'test_comment_log_entries', repositoryClass: LogEntryRepository::class)] class Comment extends AbstractLogEntry { } diff --git a/tests/Gedmo/Loggable/Fixture/Document/RelatedArticle.php b/tests/Gedmo/Loggable/Fixture/Document/RelatedArticle.php index b0a216069b..08e848dc1b 100644 --- a/tests/Gedmo/Loggable/Fixture/Document/RelatedArticle.php +++ b/tests/Gedmo/Loggable/Fixture/Document/RelatedArticle.php @@ -1,70 +1,106 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Document; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; +use Gedmo\Loggable\Loggable; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document + * * @Gedmo\Loggable */ -class RelatedArticle +#[ODM\Document] +#[Gedmo\Loggable] +class RelatedArticle implements Loggable { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** * @Gedmo\Versioned - * @ODM\String + * + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: Type::STRING)] + #[Gedmo\Versioned] + private ?string $title = null; /** * @Gedmo\Versioned - * @ODM\String + * + * @ODM\Field(type="string") */ - private $content; + #[ODM\Field(type: Type::STRING)] + #[Gedmo\Versioned] + private ?string $content = null; /** - * @ODM\ReferenceMany(targetDocument="Comment", mappedBy="article") + * @var Collection + * + * @ODM\ReferenceMany(targetDocument="Gedmo\Tests\Loggable\Fixture\Document\Comment", mappedBy="article") */ + #[ODM\ReferenceMany(targetDocument: Comment::class, mappedBy: 'article')] private $comments; - public function getId() + public function __construct() + { + $this->comments = new ArrayCollection(); + } + + public function getId(): ?string { return $this->id; } - public function addComment(Comment $comment) + public function addComment(Comment $comment): void { $comment->setArticle($this); $this->comments[] = $comment; } - public function getComments() + /** + * @return Collection + */ + public function getComments(): Collection { return $this->comments; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setContent($content) + public function setContent(?string $content): void { $this->content = $content; } - public function getContent() + public function getContent(): ?string { return $this->content; } diff --git a/tests/Gedmo/Loggable/Fixture/Entity/Address.php b/tests/Gedmo/Loggable/Fixture/Entity/Address.php index 2f77aef179..1b9f4ee86f 100644 --- a/tests/Gedmo/Loggable/Fixture/Entity/Address.php +++ b/tests/Gedmo/Loggable/Fixture/Entity/Address.php @@ -1,112 +1,115 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Loggable; use Gedmo\Mapping\Annotation as Gedmo; /** - * Class Address - * @package Loggable\Fixture\Entity * @author Fabian Sabau * - * @ORM\Entity() - * @Gedmo\Loggable() + * @ORM\Entity + * + * @Gedmo\Loggable */ -class Address +#[ORM\Entity] +#[Gedmo\Loggable] +class Address implements Loggable { /** - * @var string $id - * @ORM\Id() - * @ORM\Column(type="string", length=36) - * @ORM\GeneratedValue(strategy="UUID") + * @var int|null + * + * @ORM\Id + * @ORM\Column(name="id", type="integer") + * @ORM\GeneratedValue(strategy="AUTO") */ + #[ORM\Id] + #[ORM\Column(name: 'id', type: Types::INTEGER)] + #[ORM\GeneratedValue(strategy: 'AUTO')] protected $id; /** - * @var string $street - * @ORM\Column(type="string", length=255) - * @Gedmo\Versioned() + * @var string|null + * + * @ORM\Column(type="string", length=191) + * + * @Gedmo\Versioned */ + #[ORM\Column(type: Types::STRING, length: 191)] + #[Gedmo\Versioned] protected $street; /** - * @var string $city - * @ORM\Column(type="string", length=255) - * @Gedmo\Versioned() + * @var string|null + * + * @ORM\Column(type="string", length=191) + * + * @Gedmo\Versioned */ + #[ORM\Column(type: Types::STRING, length: 191)] + #[Gedmo\Versioned] protected $city; /** - * @var Geo $geo - * @ORM\Embedded(class="Loggable\Fixture\Entity\Geo") - * @Gedmo\Versioned() + * @var Geo|null + * + * @ORM\Embedded(class="Gedmo\Tests\Loggable\Fixture\Entity\Geo") + * + * @Gedmo\Versioned */ + #[ORM\Embedded(class: Geo::class)] + #[Gedmo\Versioned] protected $geo; - /** - * @return string - */ - public function getId() + public function getId(): ?int { return $this->id; } - /** - * @return string - */ - public function getStreet() + public function getStreet(): string { return $this->street; } - /** - * @param string $street - * @return $this - */ - public function setStreet($street) + public function setStreet(?string $street): self { $this->street = $street; return $this; } - /** - * @return string - */ - public function getCity() + public function getCity(): ?string { return $this->city; } - /** - * @param string $city - * @return $this - */ - public function setCity($city) + public function setCity(string $city): self { $this->city = $city; return $this; } - /** - * @return Geo - */ - public function getGeo() + public function getGeo(): ?Geo { return $this->geo; } - /** - * @param Geo $geo - * @return $this - */ - public function setGeo($geo) + public function setGeo(?Geo $geo): self { $this->geo = $geo; return $this; } - } diff --git a/tests/Gedmo/Loggable/Fixture/Entity/Article.php b/tests/Gedmo/Loggable/Fixture/Entity/Article.php index 49a7effed9..dfb9bd209b 100644 --- a/tests/Gedmo/Loggable/Fixture/Entity/Article.php +++ b/tests/Gedmo/Loggable/Fixture/Entity/Article.php @@ -1,40 +1,62 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Loggable; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\Loggable */ -class Article +#[ORM\Entity] +#[Gedmo\Loggable] +class Article implements Loggable { /** + * @var int|null + * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ + #[ORM\Id] + #[ORM\Column(name: 'id', type: Types::INTEGER)] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] private $id; /** * @Gedmo\Versioned + * * @ORM\Column(name="title", type="string", length=8) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 8)] + #[Gedmo\Versioned] + private ?string $title = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Loggable/Fixture/Entity/Comment.php b/tests/Gedmo/Loggable/Fixture/Entity/Comment.php index b0627d01af..b50e2289ef 100644 --- a/tests/Gedmo/Loggable/Fixture/Entity/Comment.php +++ b/tests/Gedmo/Loggable/Fixture/Entity/Comment.php @@ -1,72 +1,101 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Loggable; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tests\Loggable\Fixture\Entity\Log\Comment as CommentLog; /** * @ORM\Entity - * @Gedmo\Loggable(logEntryClass="Loggable\Fixture\Entity\Log\Comment") + * + * @Gedmo\Loggable(logEntryClass="Gedmo\Tests\Loggable\Fixture\Entity\Log\Comment") */ -class Comment +#[ORM\Entity] +#[Gedmo\Loggable(logEntryClass: CommentLog::class)] +class Comment implements Loggable { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + #[ORM\GeneratedValue] private $id; /** * @Gedmo\Versioned + * * @ORM\Column(length=128) */ - private $subject; + #[ORM\Column(length: 128)] + #[Gedmo\Versioned] + private ?string $subject = null; /** * @Gedmo\Versioned + * * @ORM\Column(type="text") */ - private $message; + #[ORM\Column(type: Types::TEXT)] + #[Gedmo\Versioned] + private ?string $message = null; /** * @Gedmo\Versioned + * * @ORM\ManyToOne(targetEntity="RelatedArticle", inversedBy="comments") */ - private $article; + #[ORM\ManyToOne(targetEntity: RelatedArticle::class, inversedBy: 'comments')] + #[Gedmo\Versioned] + private ?RelatedArticle $article = null; - public function setArticle($article) + public function setArticle(?RelatedArticle $article): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?RelatedArticle { return $this->article; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setSubject($subject) + public function setSubject(?string $subject): void { $this->subject = $subject; } - public function getSubject() + public function getSubject(): ?string { return $this->subject; } - public function setMessage($message) + public function setMessage(?string $message): void { $this->message = $message; } - public function getMessage() + public function getMessage(): ?string { return $this->message; } diff --git a/tests/Gedmo/Loggable/Fixture/Entity/Composite.php b/tests/Gedmo/Loggable/Fixture/Entity/Composite.php new file mode 100644 index 0000000000..c631d2a56c --- /dev/null +++ b/tests/Gedmo/Loggable/Fixture/Entity/Composite.php @@ -0,0 +1,75 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * + * @Gedmo\Loggable + */ +#[ORM\Entity] +#[Gedmo\Loggable] +class Composite +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\Column(name: 'one', type: Types::INTEGER)] + private int $one; + + /** + * @ORM\Id + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\Column(name: 'two', type: Types::INTEGER)] + private int $two; + + /** + * @ORM\Column(length=8) + * + * @Gedmo\Versioned + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 8)] + #[Gedmo\Versioned] + private ?string $title = null; + + public function __construct(int $one, int $two) + { + $this->one = $one; + $this->two = $two; + } + + public function getOne(): int + { + return $this->one; + } + + public function getTwo(): int + { + return $this->two; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/tests/Gedmo/Loggable/Fixture/Entity/CompositeRelation.php b/tests/Gedmo/Loggable/Fixture/Entity/CompositeRelation.php new file mode 100644 index 0000000000..0cde9c3c4a --- /dev/null +++ b/tests/Gedmo/Loggable/Fixture/Entity/CompositeRelation.php @@ -0,0 +1,75 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * + * @Gedmo\Loggable + */ +#[ORM\Entity] +#[Gedmo\Loggable] +class CompositeRelation +{ + /** + * @ORM\Id + * @ORM\ManyToOne(targetEntity="Article") + */ + #[ORM\Id] + #[ORM\ManyToOne(targetEntity: Article::class)] + private Article $articleOne; + + /** + * @ORM\Id + * @ORM\ManyToOne(targetEntity="Article") + */ + #[ORM\Id] + #[ORM\ManyToOne(targetEntity: Article::class)] + private Article $articleTwo; + + /** + * @ORM\Column(length=8) + * + * @Gedmo\Versioned + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 8)] + #[Gedmo\Versioned] + private ?string $title = null; + + public function __construct(Article $articleOne, Article $articleTwo) + { + $this->articleOne = $articleOne; + $this->articleTwo = $articleTwo; + } + + public function getArticleOne(): Article + { + return $this->articleOne; + } + + public function getArticleTwo(): Article + { + return $this->articleTwo; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/tests/Gedmo/Loggable/Fixture/Entity/Geo.php b/tests/Gedmo/Loggable/Fixture/Entity/Geo.php index a56d195ff3..136fbb6bcc 100644 --- a/tests/Gedmo/Loggable/Fixture/Entity/Geo.php +++ b/tests/Gedmo/Loggable/Fixture/Entity/Geo.php @@ -1,73 +1,109 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** * Class Geo - * @package Loggable\Fixture + * * @author Fabian Sabau * - * @ORM\Embeddable() + * @ORM\Embeddable */ +#[ORM\Embeddable] class Geo { /** - * @var string $latitude + * @var string|null + * + * @phpstan-var numeric-string|null + * * @ORM\Column(type="decimal", precision=9, scale=6) - * @Gedmo\Versioned() + * + * @Gedmo\Versioned */ + #[ORM\Column(type: Types::DECIMAL, precision: 9, scale: 6)] + #[Gedmo\Versioned] protected $latitude; /** - * @var string $longitude + * @var string|null + * + * @phpstan-var numeric-string|null + * * @ORM\Column(type="decimal", precision=9, scale=6) - * @Gedmo\Versioned() + * + * @Gedmo\Versioned */ + #[ORM\Column(type: Types::DECIMAL, precision: 9, scale: 6)] + #[Gedmo\Versioned] protected $longitude; /** - * Geo constructor. - * @param string $latitude - * @param string $longitude + * @var GeoLocation + * + * @ORM\Embedded(class="Gedmo\Tests\Loggable\Fixture\Entity\GeoLocation") + * + * @Gedmo\Versioned */ - public function __construct($latitude, $longitude) + #[ORM\Embedded(class: GeoLocation::class)] + #[Gedmo\Versioned] + protected $geoLocation; + + public function __construct(float $latitude, float $longitude, GeoLocation $geoLocation) { - $this->latitude = $latitude; - $this->longitude = $longitude; + $this->latitude = $this->parseFloatToString($latitude); + $this->longitude = $this->parseFloatToString($longitude); + $this->geoLocation = $geoLocation; } - /** - * @return string - */ - public function getLatitude() + public function getLatitude(): float { - return $this->latitude; + return (float) $this->latitude; } - /** - * @param string $latitude - */ - public function setLatitude($latitude) + public function setLatitude(float $latitude): void { - $this->latitude = $latitude; + $this->latitude = $this->parseFloatToString($latitude); } - /** - * @return string - */ - public function getLongitude() + public function getLongitude(): float + { + return (float) $this->longitude; + } + + public function setLongitude(float $longitude): void + { + $this->longitude = $this->parseFloatToString($longitude); + } + + public function getGeoLocation(): GeoLocation + { + return $this->geoLocation; + } + + public function setGeoLocation(GeoLocation $geoLocation): void { - return $this->longitude; + $this->geoLocation = $geoLocation; } /** - * @param string $longitude + * @phpstan-return numeric-string */ - public function setLongitude($longitude) + private function parseFloatToString(float $number): string { - $this->longitude = $longitude; + return sprintf('%.6f', $number); } } diff --git a/tests/Gedmo/Loggable/Fixture/Entity/GeoLocation.php b/tests/Gedmo/Loggable/Fixture/Entity/GeoLocation.php new file mode 100644 index 0000000000..0495de48df --- /dev/null +++ b/tests/Gedmo/Loggable/Fixture/Entity/GeoLocation.php @@ -0,0 +1,53 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * Class GeoLocation + * + * @author Fabian Sabau + * + * @ORM\Embeddable + */ +#[ORM\Embeddable] +class GeoLocation +{ + /** + * @var string + * + * @ORM\Column(type="string") + * + * @Gedmo\Versioned + */ + #[ORM\Column(type: Types::STRING)] + #[Gedmo\Versioned] + protected $location; + + public function __construct(string $location) + { + $this->location = $location; + } + + public function getLocation(): string + { + return $this->location; + } + + public function setLocation(string $location): void + { + $this->location = $location; + } +} diff --git a/tests/Gedmo/Loggable/Fixture/Entity/Log/Comment.php b/tests/Gedmo/Loggable/Fixture/Entity/Log/Comment.php index 13553c1578..79625f2a4b 100644 --- a/tests/Gedmo/Loggable/Fixture/Entity/Log/Comment.php +++ b/tests/Gedmo/Loggable/Fixture/Entity/Log/Comment.php @@ -1,14 +1,29 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Entity\Log; -use Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry; +use Gedmo\Loggable\Entity\Repository\LogEntryRepository; +use Gedmo\Tests\Loggable\Fixture\Document\Comment as CommentEntity; /** * @ORM\Table(name="test_comment_log_entries") * @ORM\Entity(repositoryClass="Gedmo\Loggable\Entity\Repository\LogEntryRepository") + * + * @phpstan-extends AbstractLogEntry */ +#[ORM\Table(name: 'test_comment_log_entries')] +#[ORM\Entity(repositoryClass: LogEntryRepository::class)] class Comment extends AbstractLogEntry { } diff --git a/tests/Gedmo/Loggable/Fixture/Entity/RelatedArticle.php b/tests/Gedmo/Loggable/Fixture/Entity/RelatedArticle.php index 91814db63e..a111553d5a 100644 --- a/tests/Gedmo/Loggable/Fixture/Entity/RelatedArticle.php +++ b/tests/Gedmo/Loggable/Fixture/Entity/RelatedArticle.php @@ -1,72 +1,110 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable\Fixture\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Loggable; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\Loggable */ -class RelatedArticle +#[ORM\Entity] +#[Gedmo\Loggable] +class RelatedArticle implements Loggable { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + #[ORM\GeneratedValue()] private $id; /** * @Gedmo\Versioned + * * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(length: 128)] + #[Gedmo\Versioned] + private ?string $title = null; /** * @Gedmo\Versioned + * * @ORM\Column(type="text") */ - private $content; + #[ORM\Column(Types::TEXT)] + #[Gedmo\Versioned] + private ?string $content = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Comment", mappedBy="article") */ + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article')] private $comments; - public function getId() + public function __construct() + { + $this->comments = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function addComment(Comment $comment) + public function addComment(Comment $comment): void { $comment->setArticle($this); $this->comments[] = $comment; } - public function getComments() + /** + * @return Collection + */ + public function getComments(): Collection { return $this->comments; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setContent($content) + public function setContent(?string $content): void { $this->content = $content; } - public function getContent() + public function getContent(): ?string { return $this->content; } diff --git a/tests/Gedmo/Loggable/LoggableDocumentTest.php b/tests/Gedmo/Loggable/LoggableDocumentTest.php index a0ca70bbf8..2d86eb2a46 100644 --- a/tests/Gedmo/Loggable/LoggableDocumentTest.php +++ b/tests/Gedmo/Loggable/LoggableDocumentTest.php @@ -1,32 +1,36 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Loggable\Fixture\Document\Article; -use Loggable\Fixture\Document\RelatedArticle; -use Loggable\Fixture\Document\Comment; -use Loggable\Fixture\Document\Author; -use Composer\Autoload\ClassLoader; +use Gedmo\Loggable\Document\LogEntry; +use Gedmo\Loggable\Document\Repository\LogEntryRepository; +use Gedmo\Loggable\LoggableListener; +use Gedmo\Tests\Loggable\Fixture\Document\Article; +use Gedmo\Tests\Loggable\Fixture\Document\Author; +use Gedmo\Tests\Loggable\Fixture\Document\Comment; +use Gedmo\Tests\Loggable\Fixture\Document\Log\Comment as CommentLog; +use Gedmo\Tests\Loggable\Fixture\Document\RelatedArticle; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for loggable behavior * * @author Boussekeyt Jules * @author Gediminas Morkevicius - * - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class LoggableDocumentTest extends BaseTestCaseMongoODM +final class LoggableDocumentTest extends BaseTestCaseMongoODM { - const ARTICLE = 'Loggable\\Fixture\\Document\\Article'; - const COMMENT = 'Loggable\\Fixture\\Document\\Comment'; - const RELATED_ARTICLE = 'Loggable\\Fixture\\Document\\RelatedArticle'; - const COMMENT_LOG = 'Loggable\\Fixture\\Document\\Log\\Comment'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); @@ -34,14 +38,14 @@ protected function setUp() $loggableListener->setUsername('jules'); $evm->addEventSubscriber($loggableListener); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); } - public function testLogGeneration() + public function testLogGeneration(): void { - $logRepo = $this->dm->getRepository('Gedmo\\Loggable\\Document\\LogEntry'); - $articleRepo = $this->dm->getRepository(self::ARTICLE); - $this->assertCount(0, $logRepo->findAll()); + $logRepo = $this->dm->getRepository(LogEntry::class); + $articleRepo = $this->dm->getRepository(Article::class); + static::assertCount(0, $logRepo->findAll()); $art0 = new Article(); $art0->setTitle('Title'); @@ -55,73 +59,73 @@ public function testLogGeneration() $this->dm->persist($art0); $this->dm->flush(); - $log = $logRepo->findOneByObjectId($art0->getId()); + $log = $logRepo->findOneBy(['objectId' => $art0->getId()]); - $this->assertNotNull($log); - $this->assertEquals('create', $log->getAction()); - $this->assertEquals(get_class($art0), $log->getObjectClass()); - $this->assertEquals('jules', $log->getUsername()); - $this->assertEquals(1, $log->getVersion()); + static::assertNotNull($log); + static::assertSame('create', $log->getAction()); + static::assertSame(get_class($art0), $log->getObjectClass()); + static::assertSame('jules', $log->getUsername()); + static::assertSame(1, $log->getVersion()); $data = $log->getData(); - $this->assertCount(2, $data); - $this->assertArrayHasKey('title', $data); - $this->assertEquals($data['title'], 'Title'); - $this->assertArrayHasKey('author', $data); - $this->assertEquals($data['author'], array('name' => 'John Doe', 'email' => 'john@doe.com')); + static::assertCount(2, $data); + static::assertArrayHasKey('title', $data); + static::assertSame('Title', $data['title']); + static::assertArrayHasKey('author', $data); + static::assertSame(['name' => 'John Doe', 'email' => 'john@doe.com'], $data['author']); // test update - $article = $articleRepo->findOneByTitle('Title'); + $article = $articleRepo->findOneBy(['title' => 'Title']); $article->setTitle('New'); $this->dm->persist($article); $this->dm->flush(); $this->dm->clear(); - $log = $logRepo->findOneBy(array('version' => 2, 'objectId' => $article->getId())); - $this->assertEquals('update', $log->getAction()); + $log = $logRepo->findOneBy(['version' => 2, 'objectId' => $article->getId()]); + static::assertSame('update', $log->getAction()); // test delete - $article = $articleRepo->findOneByTitle('New'); + $article = $articleRepo->findOneBy(['title' => 'New']); $this->dm->remove($article); $this->dm->flush(); $this->dm->clear(); - $log = $logRepo->findOneBy(array('version' => 3, 'objectId' => $article->getId())); - $this->assertEquals('remove', $log->getAction()); - $this->assertNull($log->getData()); + $log = $logRepo->findOneBy(['version' => 3, 'objectId' => $article->getId()]); + static::assertSame('remove', $log->getAction()); + static::assertNull($log->getData()); } - public function testVersionControl() + public function testVersionControl(): void { $this->populate(); - $commentLogRepo = $this->dm->getRepository(self::COMMENT_LOG); - $commentRepo = $this->dm->getRepository(self::COMMENT); + $commentLogRepo = $this->dm->getRepository(CommentLog::class); + $commentRepo = $this->dm->getRepository(Comment::class); + static::assertInstanceOf(LogEntryRepository::class, $commentLogRepo); - $comment = $commentRepo->findOneByMessage('m-v5'); - $commentId = $comment->getId(); - $this->assertEquals('m-v5', $comment->getMessage()); - $this->assertEquals('s-v3', $comment->getSubject()); - $this->assertEquals('a2-t-v1', $comment->getArticle()->getTitle()); - $this->assertEquals('Jane Doe', $comment->getAuthor()->getName()); - $this->assertEquals('jane@doe.com', $comment->getAuthor()->getEmail()); + $comment = $commentRepo->findOneBy(['message' => 'm-v5']); + static::assertSame('m-v5', $comment->getMessage()); + static::assertSame('s-v3', $comment->getSubject()); + static::assertSame('a2-t-v1', $comment->getArticle()->getTitle()); + static::assertSame('Jane Doe', $comment->getAuthor()->getName()); + static::assertSame('jane@doe.com', $comment->getAuthor()->getEmail()); // test revert $commentLogRepo->revert($comment, 3); - $this->assertEquals('s-v3', $comment->getSubject()); - $this->assertEquals('m-v2', $comment->getMessage()); - $this->assertEquals('a1-t-v1', $comment->getArticle()->getTitle()); - $this->assertEquals('John Doe', $comment->getAuthor()->getName()); - $this->assertEquals('john@doe.com', $comment->getAuthor()->getEmail()); + static::assertSame('s-v3', $comment->getSubject()); + static::assertSame('m-v2', $comment->getMessage()); + static::assertSame('a1-t-v1', $comment->getArticle()->getTitle()); + static::assertSame('John Doe', $comment->getAuthor()->getName()); + static::assertSame('john@doe.com', $comment->getAuthor()->getEmail()); $this->dm->persist($comment); $this->dm->flush(); // test get log entries $logEntries = $commentLogRepo->getLogEntries($comment); - $this->assertCount(6, $logEntries); + static::assertCount(6, $logEntries); $latest = array_shift($logEntries); - $this->assertEquals('update', $latest->getAction()); + static::assertSame('update', $latest->getAction()); } - private function populate() + private function populate(): void { $article = new RelatedArticle(); $article->setTitle('a1-t-v1'); diff --git a/tests/Gedmo/Loggable/LoggableEntityTest.php b/tests/Gedmo/Loggable/LoggableEntityTest.php index 25e6b83608..21312c45a1 100644 --- a/tests/Gedmo/Loggable/LoggableEntityTest.php +++ b/tests/Gedmo/Loggable/LoggableEntityTest.php @@ -1,48 +1,53 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Loggable; + +use Doctrine\DBAL\Types\ArrayType; +use Gedmo\Loggable\Entity\LogEntry; +use Gedmo\Loggable\Entity\Repository\LogEntryRepository; +use Gedmo\Loggable\Loggable; +use Gedmo\Loggable\LoggableListener; +use Gedmo\Tests\Loggable\Fixture\Entity\Address; +use Gedmo\Tests\Loggable\Fixture\Entity\Article; +use Gedmo\Tests\Loggable\Fixture\Entity\Comment; +use Gedmo\Tests\Loggable\Fixture\Entity\Composite; +use Gedmo\Tests\Loggable\Fixture\Entity\CompositeRelation; +use Gedmo\Tests\Loggable\Fixture\Entity\Geo; +use Gedmo\Tests\Loggable\Fixture\Entity\GeoLocation; +use Gedmo\Tests\Loggable\Fixture\Entity\Log\Comment as CommentLog; +use Gedmo\Tests\Loggable\Fixture\Entity\RelatedArticle; +use Gedmo\Tests\TestActorProvider; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for loggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class LoggableEntityTest extends BaseTestCaseORM +abstract class LoggableEntityTest extends BaseTestCaseORM { - const ARTICLE = 'Loggable\Fixture\Entity\Article'; - const COMMENT = 'Loggable\Fixture\Entity\Comment'; - const RELATED_ARTICLE = 'Loggable\Fixture\Entity\RelatedArticle'; - const COMMENT_LOG = 'Loggable\Fixture\Entity\Log\Comment'; - - private $articleId; - private $LoggableListener; + /** + * @var LoggableListener + */ + protected LoggableListener $listener; - protected function setUp() + public static function setUpBeforeClass(): void { - parent::setUp(); - - $evm = new EventManager(); - $this->LoggableListener = new LoggableListener(); - $this->LoggableListener->setUsername('jules'); - $evm->addEventSubscriber($this->LoggableListener); - - $this->em = $this->getMockSqliteEntityManager($evm); + if (!class_exists(ArrayType::class)) { + static::markTestSkipped('The loggable extension is not compatible with doctrine/dbal:>=4.0'); + } } - /** - * @test - */ - public function shouldHandleClonedEntity() + public function testShouldHandleClonedEntity(): void { $art0 = new Article(); $art0->setTitle('Title'); @@ -55,19 +60,19 @@ public function shouldHandleClonedEntity() $this->em->persist($art1); $this->em->flush(); - $logRepo = $this->em->getRepository('Gedmo\Loggable\Entity\LogEntry'); + $logRepo = $this->em->getRepository(LogEntry::class); $logs = $logRepo->findAll(); - $this->assertSame(2, count($logs)); - $this->assertSame('create', $logs[0]->getAction()); - $this->assertSame('create', $logs[1]->getAction()); - $this->assertTrue($logs[0]->getObjectId() !== $logs[1]->getObjectId()); + static::assertCount(2, $logs); + static::assertSame('create', $logs[0]->getAction()); + static::assertSame('create', $logs[1]->getAction()); + static::assertNotSame($logs[0]->getObjectId(), $logs[1]->getObjectId()); } - public function testLoggable() + public function testLoggable(): void { - $logRepo = $this->em->getRepository('Gedmo\Loggable\Entity\LogEntry'); - $articleRepo = $this->em->getRepository(self::ARTICLE); - $this->assertCount(0, $logRepo->findAll()); + $logRepo = $this->em->getRepository(LogEntry::class); + $articleRepo = $this->em->getRepository(Article::class); + static::assertCount(0, $logRepo->findAll()); $art0 = new Article(); $art0->setTitle('Title'); @@ -75,105 +80,264 @@ public function testLoggable() $this->em->persist($art0); $this->em->flush(); - $log = $logRepo->findOneByObjectId($art0->getId()); + $log = $logRepo->findOneBy(['objectId' => $art0->getId()]); - $this->assertNotNull($log); - $this->assertEquals('create', $log->getAction()); - $this->assertEquals(get_class($art0), $log->getObjectClass()); - $this->assertEquals('jules', $log->getUsername()); - $this->assertEquals(1, $log->getVersion()); + static::assertNotNull($log); + static::assertSame('create', $log->getAction()); + static::assertSame(get_class($art0), $log->getObjectClass()); + static::assertSame('jules', $log->getUsername()); + static::assertSame(1, $log->getVersion()); $data = $log->getData(); - $this->assertCount(1, $data); - $this->assertArrayHasKey('title', $data); - $this->assertEquals($data['title'], 'Title'); + static::assertCount(1, $data); + static::assertArrayHasKey('title', $data); + static::assertSame('Title', $data['title']); // test update - $article = $articleRepo->findOneByTitle('Title'); + $article = $articleRepo->findOneBy(['title' => 'Title']); $article->setTitle('New'); $this->em->persist($article); $this->em->flush(); $this->em->clear(); - $log = $logRepo->findOneBy(array('version' => 2, 'objectId' => $article->getId())); - $this->assertEquals('update', $log->getAction()); + $log = $logRepo->findOneBy(['version' => 2, 'objectId' => $article->getId()]); + static::assertSame('update', $log->getAction()); // test delete - $article = $articleRepo->findOneByTitle('New'); + $article = $articleRepo->findOneBy(['title' => 'New']); $this->em->remove($article); $this->em->flush(); $this->em->clear(); - $log = $logRepo->findOneBy(array('version' => 3, 'objectId' => 1)); - $this->assertEquals('remove', $log->getAction()); - $this->assertNull($log->getData()); + $log = $logRepo->findOneBy(['version' => 3, 'objectId' => 1]); + static::assertSame('remove', $log->getAction()); + static::assertNull($log->getData()); } - public function testVersionControl() + public function testLoggableWithActorProvider(): void + { + $this->listener->setActorProvider(new TestActorProvider('testactor')); + + $logRepo = $this->em->getRepository(LogEntry::class); + $articleRepo = $this->em->getRepository(Article::class); + static::assertCount(0, $logRepo->findAll()); + + $art0 = new Article(); + $art0->setTitle('Title'); + + $this->em->persist($art0); + $this->em->flush(); + + $log = $logRepo->findOneBy(['objectId' => $art0->getId()]); + + static::assertNotNull($log); + static::assertSame('create', $log->getAction()); + static::assertSame(get_class($art0), $log->getObjectClass()); + static::assertSame('testactor', $log->getUsername()); + static::assertSame(1, $log->getVersion()); + $data = $log->getData(); + static::assertCount(1, $data); + static::assertArrayHasKey('title', $data); + static::assertSame('Title', $data['title']); + + // test update + $article = $articleRepo->findOneBy(['title' => 'Title']); + + $article->setTitle('New'); + $this->em->persist($article); + $this->em->flush(); + $this->em->clear(); + + $log = $logRepo->findOneBy(['version' => 2, 'objectId' => $article->getId()]); + static::assertSame('update', $log->getAction()); + + // test delete + $article = $articleRepo->findOneBy(['title' => 'New']); + $this->em->remove($article); + $this->em->flush(); + $this->em->clear(); + + $log = $logRepo->findOneBy(['version' => 3, 'objectId' => 1]); + static::assertSame('remove', $log->getAction()); + static::assertNull($log->getData()); + } + + public function testVersionControl(): void { $this->populate(); - $commentLogRepo = $this->em->getRepository(self::COMMENT_LOG); - $commentRepo = $this->em->getRepository(self::COMMENT); + /** @var LogEntryRepository $commentLogRepo */ + $commentLogRepo = $this->em->getRepository(CommentLog::class); + $commentRepo = $this->em->getRepository(Comment::class); $comment = $commentRepo->find(1); - $this->assertEquals('m-v5', $comment->getMessage()); - $this->assertEquals('s-v3', $comment->getSubject()); - $this->assertEquals(2, $comment->getArticle()->getId()); + static::assertInstanceOf(Comment::class, $comment); + static::assertSame('m-v5', $comment->getMessage()); + static::assertSame('s-v3', $comment->getSubject()); + static::assertSame(2, $comment->getArticle()->getId()); // test revert $commentLogRepo->revert($comment, 3); - $this->assertEquals('s-v3', $comment->getSubject()); - $this->assertEquals('m-v2', $comment->getMessage()); - $this->assertEquals(1, $comment->getArticle()->getId()); + static::assertSame('s-v3', $comment->getSubject()); + static::assertSame('m-v2', $comment->getMessage()); + static::assertSame(1, $comment->getArticle()->getId()); $this->em->persist($comment); $this->em->flush(); // test get log entries $logEntries = $commentLogRepo->getLogEntries($comment); - $this->assertCount(6, $logEntries); + static::assertCount(6, $logEntries); $latest = $logEntries[0]; - $this->assertEquals('update', $latest->getAction()); + static::assertSame('update', $latest->getAction()); } - public function testLogEmbedded() + public function testLogEmbedded(): void { $address = $this->populateEmbedded(); - - $logRepo = $this->em->getRepository('Gedmo\Loggable\Entity\LogEntry'); + /** @var LogEntryRepository
        $logRepo */ + $logRepo = $this->em->getRepository(LogEntry::class); $logEntries = $logRepo->getLogEntries($address); - $this->assertCount(4, $logEntries); + static::assertCount(4, $logEntries); + static::assertCount(1, $logEntries[0]->getData()); + static::assertCount(2, $logEntries[1]->getData()); + static::assertCount(3, $logEntries[2]->getData()); + static::assertCount(5, $logEntries[3]->getData()); + } + + public function testComposite(): void + { + $logRepo = $this->em->getRepository(LogEntry::class); + $compositeRepo = $this->em->getRepository(Composite::class); + static::assertCount(0, $logRepo->findAll()); + + $compositeIds = [1, 2]; + + $cmp = new Composite(...$compositeIds); + $cmp->setTitle('Title2'); + + $this->em->persist($cmp); + $this->em->flush(); + + $cmpId = sprintf('%s %s', ...$compositeIds); + + $log = $logRepo->findOneBy(['objectId' => $cmpId]); + + static::assertNotNull($log); + static::assertSame('create', $log->getAction()); + static::assertSame(get_class($cmp), $log->getObjectClass()); + static::assertSame('jules', $log->getUsername()); + static::assertSame(1, $log->getVersion()); + $data = $log->getData(); + static::assertCount(1, $data); + static::assertArrayHasKey('title', $data); + static::assertSame($data['title'], 'Title2'); + + // test update + $composite = $compositeRepo->findOneBy(['title' => 'Title2']); + + $composite->setTitle('New'); + $this->em->persist($composite); + $this->em->flush(); + $this->em->clear(); + + $log = $logRepo->findOneBy(['version' => 2, 'objectId' => $cmpId]); + static::assertSame('update', $log->getAction()); + + // test delete + $composite = $compositeRepo->findOneBy(['title' => 'New']); + $this->em->remove($composite); + $this->em->flush(); + $this->em->clear(); + + $log = $logRepo->findOneBy(['version' => 3, 'objectId' => $cmpId]); + static::assertSame('remove', $log->getAction()); + static::assertNull($log->getData()); + } + + public function testCompositeRelation(): void + { + $logRepo = $this->em->getRepository(LogEntry::class); + $compositeRepo = $this->em->getRepository(CompositeRelation::class); + static::assertCount(0, $logRepo->findAll()); + + $art0 = new Article(); + $art0->setTitle('Title0'); + $art1 = new Article(); + $art1->setTitle('Title1'); + $cmp0 = new CompositeRelation($art0, $art1); + $cmp0->setTitle('Title2'); + + $this->em->persist($art0); + $this->em->persist($art1); + $this->em->persist($cmp0); + $this->em->flush(); + + $cmpId = sprintf('%s %s', $art0->getId(), $art1->getId()); + + $log = $logRepo->findOneBy(['objectId' => $cmpId]); + + static::assertNotNull($log); + static::assertSame('create', $log->getAction()); + static::assertSame(get_class($cmp0), $log->getObjectClass()); + static::assertSame('jules', $log->getUsername()); + static::assertSame(1, $log->getVersion()); + $data = $log->getData(); + static::assertCount(1, $data); + static::assertArrayHasKey('title', $data); + static::assertSame($data['title'], 'Title2'); + + // test update + $composite = $compositeRepo->findOneBy(['title' => 'Title2']); + + $composite->setTitle('New'); + $this->em->persist($composite); + $this->em->flush(); + $this->em->clear(); + + $log = $logRepo->findOneBy(['version' => 2, 'objectId' => $cmpId]); + static::assertSame('update', $log->getAction()); + + // test delete + $composite = $compositeRepo->findOneBy(['title' => 'New']); + $this->em->remove($composite); + $this->em->flush(); + $this->em->clear(); + $log = $logRepo->findOneBy(['version' => 3, 'objectId' => $cmpId]); + static::assertSame('remove', $log->getAction()); + static::assertNull($log->getData()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::COMMENT, - self::COMMENT_LOG, - self::RELATED_ARTICLE, - 'Gedmo\Loggable\Entity\LogEntry', - 'Loggable\Fixture\Entity\Address', - 'Loggable\Fixture\Entity\Geo', - ); + return [ + Article::class, + Comment::class, + CommentLog::class, + RelatedArticle::class, + Composite::class, + CompositeRelation::class, + LogEntry::class, + Address::class, + Geo::class, + ]; } - private function populateEmbedded() + private function populateEmbedded(): Address { $address = new Address(); $address->setCity('city-v1'); $address->setStreet('street-v1'); - $geo = new Geo(1.0000, 1.0000); + $geo = new Geo(1.0000, 1.0000, new GeoLocation('Online')); $address->setGeo($geo); $this->em->persist($address); $this->em->flush(); - $geo2 = new Geo(2.0000, 2.0000); + $geo2 = new Geo(2.0000, 2.0000, new GeoLocation('Offline')); $address->setGeo($geo2); $this->em->persist($address); @@ -193,7 +357,7 @@ private function populateEmbedded() return $address; } - private function populate() + private function populate(): void { $article = new RelatedArticle(); $article->setTitle('a1-t-v1'); diff --git a/tests/Gedmo/Mapping/Annotation/AnnotationArgumentsTest.php b/tests/Gedmo/Mapping/Annotation/AnnotationArgumentsTest.php new file mode 100644 index 0000000000..edb8bfeed7 --- /dev/null +++ b/tests/Gedmo/Mapping/Annotation/AnnotationArgumentsTest.php @@ -0,0 +1,74 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Annotation; + +use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; +use Gedmo\Mapping\Annotation\Annotation; +use Gedmo\Mapping\Annotation\Blameable; +use PHPUnit\Framework\TestCase; + +/** + * Remove this class when support for array based attributes in annotation classes is removed. + * + * @group legacy + */ +final class AnnotationArgumentsTest extends TestCase +{ + use VerifyDeprecations; + + /** + * @param array $expected + * @param mixed[] $args + * + * @dataProvider getGedmoAnnotations + * + * @param class-string $class + */ + public function testArguments(array $expected, string $class, array $args, ?string $expectedDeprecationIdentifier = null): void + { + if (null !== $expectedDeprecationIdentifier) { + $this->expectDeprecationWithIdentifier($expectedDeprecationIdentifier); + } + + $annotation = new $class(...$args); + + foreach ($expected as $attribute => $value) { + static::assertSame($value, $annotation->$attribute); + } + } + + /** + * @phpstan-return iterable, 1: class-string, 2: array|string>, 3?: string}> + */ + public static function getGedmoAnnotations(): iterable + { + yield 'args_without_data' => [['on' => 'delete', 'field' => 'some'], Blameable::class, [[], 'delete', 'some']]; + yield 'default_values_without_args' => [['on' => 'update', 'field' => null, 'value' => null], Blameable::class, []]; + yield 'default_values_with_args' => [['on' => 'update', 'field' => null, 'value' => null], Blameable::class, [[], 'update']]; + + yield 'args_with_data' => [ + ['on' => 'delete', 'field' => 'some'], + Blameable::class, [['on' => 'change', 'field' => 'id'], 'delete', 'some'], + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2375', + ]; + yield 'data_without_args' => [ + ['on' => 'change', 'field' => 'id'], + Blameable::class, [['on' => 'change', 'field' => 'id']], + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2375', + ]; + yield 'default_values_with_args_and_data' => [ + ['on' => 'update', 'field' => null, 'value' => null], + Blameable::class, [['on' => 'change'], 'update'], + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2375', + ]; + } +} diff --git a/tests/Gedmo/Mapping/Annotation/BaseClassAnnotationTestCase.php b/tests/Gedmo/Mapping/Annotation/BaseClassAnnotationTestCase.php new file mode 100644 index 0000000000..86c07a1276 --- /dev/null +++ b/tests/Gedmo/Mapping/Annotation/BaseClassAnnotationTestCase.php @@ -0,0 +1,75 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Annotation; + +use Doctrine\Common\Annotations\AnnotationReader; +use Gedmo\Mapping\Annotation\Annotation; +use PHPUnit\Framework\TestCase; + +abstract class BaseClassAnnotationTestCase extends TestCase +{ + /** + * @requires PHP 8 + * + * @dataProvider getValidParameters + * + * @param mixed $expectedReturn + */ + public function testLoadFromAttribute(string $annotationProperty, $expectedReturn): void + { + $annotation = $this->getClassAnnotation(true); + static::assertSame($annotation->$annotationProperty, $expectedReturn); + } + + /** + * @dataProvider getValidParameters + * + * @param mixed $expectedReturn + */ + public function testLoadFromDoctrineAnnotation(string $annotationProperty, $expectedReturn): void + { + $annotation = $this->getClassAnnotation(false); + static::assertSame($annotation->$annotationProperty, $expectedReturn); + } + + /** + * @phpstan-return iterable + */ + abstract public static function getValidParameters(): iterable; + + abstract protected function getAnnotationClass(): string; + + abstract protected function getAttributeModelClass(): string; + + abstract protected function getAnnotationModelClass(): string; + + private function getClassAnnotation(bool $attributes): Annotation + { + $class = $attributes ? $this->getAttributeModelClass() : $this->getAnnotationModelClass(); + $reflection = new \ReflectionClass($class); + $annotationClass = $this->getAnnotationClass(); + + if ($attributes) { + $attributes = $reflection->getAttributes($annotationClass); + $annotation = $attributes[0]->newInstance(); + } else { + $reader = new AnnotationReader(); + $annotation = $reader->getClassAnnotation($reflection, $annotationClass); + } + + if (!is_a($annotation, $annotationClass)) { + throw new \LogicException('Can\'t parse annotation.'); + } + + return $annotation; + } +} diff --git a/tests/Gedmo/Mapping/Annotation/BasePropertyAnnotationTestCase.php b/tests/Gedmo/Mapping/Annotation/BasePropertyAnnotationTestCase.php new file mode 100644 index 0000000000..094c439a57 --- /dev/null +++ b/tests/Gedmo/Mapping/Annotation/BasePropertyAnnotationTestCase.php @@ -0,0 +1,75 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Annotation; + +use Doctrine\Common\Annotations\AnnotationReader; +use Gedmo\Mapping\Annotation\Annotation; +use PHPUnit\Framework\TestCase; + +abstract class BasePropertyAnnotationTestCase extends TestCase +{ + /** + * @requires PHP 8 + * + * @dataProvider getValidParameters + * + * @param mixed $expectedReturn + */ + public function testLoadFromAttribute(string $annotationProperty, string $classProperty, $expectedReturn): void + { + $annotation = $this->getMethodAnnotation($classProperty, true); + static::assertSame($annotation->$annotationProperty, $expectedReturn); + } + + /** + * @dataProvider getValidParameters + * + * @param mixed $expectedReturn + */ + public function testLoadFromDoctrineAnnotation(string $annotationProperty, string $classProperty, $expectedReturn): void + { + $annotation = $this->getMethodAnnotation($classProperty, false); + static::assertSame($annotation->$annotationProperty, $expectedReturn); + } + + /** + * @phpstan-return iterable + */ + abstract public function getValidParameters(): iterable; + + abstract protected function getAnnotationClass(): string; + + abstract protected function getAttributeModelClass(): string; + + abstract protected function getAnnotationModelClass(): string; + + private function getMethodAnnotation(string $property, bool $attributes): Annotation + { + $class = $attributes ? $this->getAttributeModelClass() : $this->getAnnotationModelClass(); + $reflection = new \ReflectionProperty($class, $property); + $annotationClass = $this->getAnnotationClass(); + + if ($attributes) { + $attributes = $reflection->getAttributes($annotationClass); + $annotation = $attributes[0]->newInstance(); + } else { + $reader = new AnnotationReader(); + $annotation = $reader->getPropertyAnnotation($reflection, $annotationClass); + } + + if (!is_a($annotation, $annotationClass)) { + throw new \LogicException('Can\'t parse annotation.'); + } + + return $annotation; + } +} diff --git a/tests/Gedmo/Mapping/Annotation/TranslatablePropertyTestCase.php b/tests/Gedmo/Mapping/Annotation/TranslatablePropertyTestCase.php new file mode 100644 index 0000000000..3b5049fb28 --- /dev/null +++ b/tests/Gedmo/Mapping/Annotation/TranslatablePropertyTestCase.php @@ -0,0 +1,43 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Annotation; + +use Gedmo\Mapping\Annotation\Translatable; +use Gedmo\Tests\Mapping\Fixture\Annotation\TranslatableModel as AnnotationTranslatableModel; +use Gedmo\Tests\Mapping\Fixture\Attribute\TranslatableModel as AttributeTranslatableModel; + +final class TranslatablePropertyTestCase extends BasePropertyAnnotationTestCase +{ + public function getValidParameters(): iterable + { + return [ + ['fallback', 'title', null], + ['fallback', 'titleFallbackTrue', true], + ['fallback', 'titleFallbackFalse', false], + ]; + } + + protected function getAnnotationClass(): string + { + return Translatable::class; + } + + protected function getAttributeModelClass(): string + { + return AttributeTranslatableModel::class; + } + + protected function getAnnotationModelClass(): string + { + return AnnotationTranslatableModel::class; + } +} diff --git a/tests/Gedmo/Mapping/Annotation/TranslationEntityTestCase.php b/tests/Gedmo/Mapping/Annotation/TranslationEntityTestCase.php new file mode 100644 index 0000000000..d049b2d393 --- /dev/null +++ b/tests/Gedmo/Mapping/Annotation/TranslationEntityTestCase.php @@ -0,0 +1,41 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Annotation; + +use Gedmo\Mapping\Annotation\TranslationEntity; +use Gedmo\Tests\Mapping\Fixture\Annotation\TranslationEntityModel as AnnotationTranslationEntityModel; +use Gedmo\Tests\Mapping\Fixture\Attribute\TranslationEntityModel as AttributeTranslationEntityModel; + +final class TranslationEntityTestCase extends BaseClassAnnotationTestCase +{ + public static function getValidParameters(): iterable + { + return [ + ['class', \stdClass::class], + ]; + } + + protected function getAnnotationClass(): string + { + return TranslationEntity::class; + } + + protected function getAttributeModelClass(): string + { + return AttributeTranslationEntityModel::class; + } + + protected function getAnnotationModelClass(): string + { + return AnnotationTranslationEntityModel::class; + } +} diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.ClosureTree.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.ClosureTree.dcm.xml similarity index 66% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.ClosureTree.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.ClosureTree.dcm.xml index baec532d86..3275c35dd1 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.ClosureTree.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.ClosureTree.dcm.xml @@ -1,28 +1,18 @@ - - - - - + + - - - + - - - - + - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Embedded.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Embedded.dcm.xml new file mode 100644 index 0000000000..fe97e5e767 --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Embedded.dcm.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.EmbeddedTranslatable.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.EmbeddedTranslatable.dcm.xml new file mode 100644 index 0000000000..d068e2dbe7 --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.EmbeddedTranslatable.dcm.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Loggable.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Loggable.dcm.xml similarity index 76% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Loggable.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Loggable.dcm.xml index f8b363a89d..7b12b8ad66 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Loggable.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Loggable.dcm.xml @@ -1,14 +1,9 @@ - - - - - + + - @@ -16,9 +11,6 @@ - - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.LoggableComposite.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.LoggableComposite.dcm.xml new file mode 100644 index 0000000000..1eee13f615 --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.LoggableComposite.dcm.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.LoggableCompositeRelation.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.LoggableCompositeRelation.dcm.xml new file mode 100644 index 0000000000..4a35271e9f --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.LoggableCompositeRelation.dcm.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.LoggableWithEmbedded.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.LoggableWithEmbedded.dcm.xml similarity index 66% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.LoggableWithEmbedded.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.LoggableWithEmbedded.dcm.xml index 201ec6c03e..50cf86d79e 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.LoggableWithEmbedded.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.LoggableWithEmbedded.dcm.xml @@ -1,18 +1,12 @@ - - - - - + + - - - + + - @@ -20,9 +14,6 @@ - - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.MaterializedPathTree.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.MaterializedPathTree.dcm.xml similarity index 66% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.MaterializedPathTree.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.MaterializedPathTree.dcm.xml index ecbcd835bb..402a60c85b 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.MaterializedPathTree.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.MaterializedPathTree.dcm.xml @@ -1,41 +1,31 @@ - - - - - + + - - - - + - - + - - + + + + - - - - + - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.NestedTree.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.NestedTree.dcm.xml similarity index 84% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.NestedTree.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.NestedTree.dcm.xml index 1ab6f20116..05be4f60bd 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.NestedTree.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.NestedTree.dcm.xml @@ -1,18 +1,12 @@ - - - - - + + - - @@ -26,14 +20,10 @@ - - - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.References.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.References.dcm.xml new file mode 100644 index 0000000000..32c8e2c6a9 --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.References.dcm.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Sluggable.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Sluggable.dcm.xml similarity index 88% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Sluggable.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Sluggable.dcm.xml index 7eb5d35bb7..2970925427 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Sluggable.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Sluggable.dcm.xml @@ -1,12 +1,9 @@ - - - + + - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.SoftDeleteable.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.SoftDeleteable.dcm.xml similarity index 52% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.SoftDeleteable.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.SoftDeleteable.dcm.xml index 3ff51659c7..f7590b6efc 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.SoftDeleteable.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.SoftDeleteable.dcm.xml @@ -1,14 +1,10 @@ - - - + + - - - + diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Sortable.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Sortable.dcm.xml similarity index 72% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Sortable.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Sortable.dcm.xml index 0132902733..d2a4c1b625 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Sortable.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Sortable.dcm.xml @@ -1,12 +1,9 @@ - - - + + - @@ -14,19 +11,19 @@ - + - + - + - + diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Status.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Status.dcm.xml similarity index 62% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Status.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Status.dcm.xml index 82aeba451f..f44780123c 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Status.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Status.dcm.xml @@ -1,14 +1,9 @@ - - - - + + - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Timestampable.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Timestampable.dcm.xml similarity index 80% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Timestampable.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Timestampable.dcm.xml index ab6fb03ad8..a53036ca07 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Timestampable.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Timestampable.dcm.xml @@ -1,13 +1,9 @@ - - - - + + - @@ -17,10 +13,8 @@ - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Translatable.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Translatable.dcm.xml similarity index 79% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Translatable.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Translatable.dcm.xml index 0b07e9fab5..d2803cc21c 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Translatable.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Translatable.dcm.xml @@ -1,14 +1,9 @@ - - - - - + + - @@ -21,9 +16,6 @@ - - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.TranslatableWithEmbedded.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.TranslatableWithEmbedded.dcm.xml similarity index 71% rename from tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.TranslatableWithEmbedded.dcm.xml rename to tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.TranslatableWithEmbedded.dcm.xml index 9483bd92d2..1ef9b0ceef 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.TranslatableWithEmbedded.dcm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.TranslatableWithEmbedded.dcm.xml @@ -1,14 +1,9 @@ - - - - - + + - @@ -21,11 +16,7 @@ - - - + - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Uploadable.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Uploadable.dcm.xml new file mode 100644 index 0000000000..fe4d7e93aa --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.Uploadable.dcm.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.User.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.User.dcm.xml new file mode 100644 index 0000000000..33358fed1c --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Xml/Gedmo.Tests.Mapping.Fixture.Xml.User.dcm.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Embedded.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Embedded.dcm.xml deleted file mode 100644 index 908a43b781..0000000000 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Embedded.dcm.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.EmbeddedTranslatable.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.EmbeddedTranslatable.dcm.xml deleted file mode 100644 index 11bf2c8a45..0000000000 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.EmbeddedTranslatable.dcm.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Uploadable.dcm.xml b/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Uploadable.dcm.xml deleted file mode 100644 index e2753fae15..0000000000 --- a/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Uploadable.dcm.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Status.orm.xml b/tests/Gedmo/Mapping/Driver/Xml/Status.orm.xml index 82aeba451f..f44780123c 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Status.orm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Status.orm.xml @@ -1,14 +1,9 @@ - - - - + + - - diff --git a/tests/Gedmo/Mapping/Driver/Xml/Timestampable.orm.xml b/tests/Gedmo/Mapping/Driver/Xml/Timestampable.orm.xml index ab6fb03ad8..a53036ca07 100644 --- a/tests/Gedmo/Mapping/Driver/Xml/Timestampable.orm.xml +++ b/tests/Gedmo/Mapping/Driver/Xml/Timestampable.orm.xml @@ -1,13 +1,9 @@ - - - - + + - @@ -17,10 +13,8 @@ - - diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.BaseCategory.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.BaseCategory.dcm.yml similarity index 90% rename from tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.BaseCategory.dcm.yml rename to tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.BaseCategory.dcm.yml index 426a7da3f0..4a988d01bc 100644 --- a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.BaseCategory.dcm.yml +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.BaseCategory.dcm.yml @@ -1,5 +1,5 @@ --- -Mapping\Fixture\Yaml\BaseCategory: +Gedmo\Tests\Mapping\Fixture\Yaml\BaseCategory: type: mappedSuperclass fields: left: diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Category.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Category.dcm.yml similarity index 74% rename from tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Category.dcm.yml rename to tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Category.dcm.yml index 78f6ef5b37..7dc177e980 100644 --- a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Category.dcm.yml +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Category.dcm.yml @@ -1,5 +1,5 @@ --- -Mapping\Fixture\Yaml\Category: +Gedmo\Tests\Mapping\Fixture\Yaml\Category: type: entity table: categories id: @@ -9,7 +9,7 @@ Mapping\Fixture\Yaml\Category: strategy: AUTO gedmo: translation: - entity: Translatable\Fixture\CategoryTranslation + entity: Gedmo\Tests\Translatable\Fixture\CategoryTranslation locale: localeField tree: type: nested @@ -31,11 +31,11 @@ Mapping\Fixture\Yaml\Category: fields: - title handlers: - "Gedmo\Sluggable\Handler\RelativeSlugHandler": + Gedmo\Sluggable\Handler\RelativeSlugHandler: relationField: parent relationSlugField: slug separator: / - "Gedmo\Sluggable\Handler\TreeSlugHandler": + Gedmo\Sluggable\Handler\TreeSlugHandler: parentRelationField: parent separator: / changed: @@ -47,13 +47,13 @@ Mapping\Fixture\Yaml\Category: value: Test manyToOne: parent: - targetEntity: Mapping\Fixture\Yaml\Category + targetEntity: Gedmo\Tests\Mapping\Fixture\Yaml\Category inversedBy: children gedmo: - treeParent oneToMany: children: - targetEntity: Mapping\Fixture\Yaml\Category + targetEntity: Gedmo\Tests\Mapping\Fixture\Yaml\Category mappedBy: parent indexes: search_idx: diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.ClosureCategory.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.ClosureCategory.dcm.yml similarity index 63% rename from tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.ClosureCategory.dcm.yml rename to tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.ClosureCategory.dcm.yml index 9ba524fc62..ad4a50db26 100644 --- a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.ClosureCategory.dcm.yml +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.ClosureCategory.dcm.yml @@ -1,5 +1,5 @@ --- -Mapping\Fixture\Yaml\ClosureCategory: +Gedmo\Tests\Mapping\Fixture\Yaml\ClosureCategory: type: entity table: closure_categories id: @@ -10,7 +10,7 @@ Mapping\Fixture\Yaml\ClosureCategory: gedmo: tree: type: closure - closure: Tree\Fixture\Closure\CategoryClosure + closure: Gedmo\Tests\Tree\Fixture\Closure\CategoryClosureWithoutMapping fields: title: type: string @@ -21,13 +21,13 @@ Mapping\Fixture\Yaml\ClosureCategory: - treeLevel manyToOne: parent: - targetEntity: Mapping\Fixture\Yaml\ClosureCategory + targetEntity: Gedmo\Tests\Mapping\Fixture\Yaml\ClosureCategory inversedBy: children gedmo: - treeParent oneToMany: children: - targetEntity: Mapping\Fixture\Yaml\ClosureCategory + targetEntity: Gedmo\Tests\Mapping\Fixture\Yaml\ClosureCategory mappedBy: parent indexes: search_idx: diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Embedded.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Embedded.dcm.yml new file mode 100644 index 0000000000..f215ab5e6d --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Embedded.dcm.yml @@ -0,0 +1,6 @@ +--- +Gedmo\Tests\Mapping\Fixture\Yaml\Embedded: + type: embeddable + fields: + subtitle: + type: string diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Loggable.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Loggable.dcm.yml new file mode 100644 index 0000000000..8f21c2ff24 --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Loggable.dcm.yml @@ -0,0 +1,17 @@ +--- +Gedmo\Tests\Mapping\Fixture\Yaml\Loggable: + type: entity + table: loggable + gedmo: + loggable: + logEntryClass: Gedmo\Loggable\Entity\LogEntry + id: + id: + type: integer + generator: + strategy: AUTO + fields: + title: + type: string + gedmo: + - versioned diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.LoggableComposite.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.LoggableComposite.dcm.yml new file mode 100644 index 0000000000..30d55862cf --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.LoggableComposite.dcm.yml @@ -0,0 +1,17 @@ +--- +Gedmo\Tests\Mapping\Fixture\Yaml\LoggableComposite: + type: entity + table: loggable_with_composite + gedmo: + loggable: + logEntryClass: Gedmo\Loggable\Entity\LogEntry + id: + one: + type: integer + two: + type: integer + fields: + title: + type: string + gedmo: + - versioned diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.LoggableCompositeRelation.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.LoggableCompositeRelation.dcm.yml new file mode 100644 index 0000000000..c63163f0f3 --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.LoggableCompositeRelation.dcm.yml @@ -0,0 +1,20 @@ +--- +Gedmo\Tests\Mapping\Fixture\Yaml\LoggableCompositeRelation: + type: entity + table: loggable_with_composite_relation + gedmo: + loggable: + logEntryClass: Gedmo\Loggable\Entity\LogEntry + id: + one: + associationKey: true + two: + type: integer + fields: + title: + type: string + gedmo: + - versioned + manyToOne: + one: + targetEntity: Loggable diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.LoggableWithEmbedded.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.LoggableWithEmbedded.dcm.yml new file mode 100644 index 0000000000..79c88d8d07 --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.LoggableWithEmbedded.dcm.yml @@ -0,0 +1,22 @@ +--- +Gedmo\Tests\Mapping\Fixture\Yaml\LoggableWithEmbedded: + type: entity + table: loggable_with_embedded + gedmo: + loggable: + logEntryClass: Gedmo\Loggable\Entity\LogEntry + id: + id: + type: integer + generator: + strategy: AUTO + fields: + title: + type: string + gedmo: + - versioned + embedded: + embedded: + class: Gedmo\Tests\Mapping\Fixture\Yaml\Embedded + gedmo: + - versioned diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.MaterializedPathCategory.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.MaterializedPathCategory.dcm.yml similarity index 83% rename from tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.MaterializedPathCategory.dcm.yml rename to tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.MaterializedPathCategory.dcm.yml index 35bfd50ce5..c9c7b02a04 100644 --- a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.MaterializedPathCategory.dcm.yml +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.MaterializedPathCategory.dcm.yml @@ -1,5 +1,5 @@ --- -Mapping\Fixture\Yaml\MaterializedPathCategory: +Gedmo\Tests\Mapping\Fixture\Yaml\MaterializedPathCategory: type: entity table: materialized_path_categories id: @@ -33,7 +33,7 @@ Mapping\Fixture\Yaml\MaterializedPathCategory: - treeLockTime manyToOne: parent: - targetEntity: Mapping\Fixture\Yaml\MaterializedPathCategory + targetEntity: Gedmo\Tests\Mapping\Fixture\Yaml\MaterializedPathCategory inversedBy: children gedmo: - treeParent diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Referenced.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Referenced.dcm.yml new file mode 100644 index 0000000000..43f4ce0971 --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Referenced.dcm.yml @@ -0,0 +1,12 @@ +--- +Gedmo\Tests\Mapping\Fixture\Yaml\Referenced: + type: entity + fields: + id: + type: id + id: true + referencer: + reference: true + type: one + targetDocument: Gedmo\Mapping\Fixture\Yaml\Referencer + inversedBy: referencedDocuments diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Referencer.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Referencer.dcm.yml new file mode 100644 index 0000000000..04398f75cc --- /dev/null +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Referencer.dcm.yml @@ -0,0 +1,14 @@ +--- +Gedmo\Tests\Mapping\Fixture\Yaml\Referencer: + type: entity + fields: + id: + type: id + id: true + referencedDocuments: + reference: true + type: many + targetDocument: Gedmo\Mapping\Fixture\Yaml\Referenced + mappedBy: referencer + gedmo: + referenceIntegrity: nullify diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.SoftDeleteable.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.SoftDeleteable.dcm.yml similarity index 83% rename from tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.SoftDeleteable.dcm.yml rename to tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.SoftDeleteable.dcm.yml index 9006806dda..d4a29f7ac2 100644 --- a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.SoftDeleteable.dcm.yml +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.SoftDeleteable.dcm.yml @@ -1,5 +1,5 @@ --- -Mapping\Fixture\Yaml\SoftDeleteable: +Gedmo\Tests\Mapping\Fixture\Yaml\SoftDeleteable: type: entity table: soft_deleteables gedmo: diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Sortable.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Sortable.dcm.yml similarity index 73% rename from tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Sortable.dcm.yml rename to tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Sortable.dcm.yml index cd334e31c9..83a1c79bf4 100644 --- a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Sortable.dcm.yml +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Sortable.dcm.yml @@ -1,5 +1,5 @@ --- -Mapping\Fixture\Yaml\Sortable: +Gedmo\Tests\Mapping\Fixture\Yaml\Sortable: type: entity table: sortables id: @@ -22,11 +22,11 @@ Mapping\Fixture\Yaml\Sortable: - sortableGroup manyToOne: sortable_group: - targetEntity: Mapping\Fixture\SortableGroup + targetEntity: Gedmo\Tests\Mapping\Fixture\SortableGroup gedmo: - sortableGroup manyToMany: sortable_groups: - targetEntity: Mapping\Fixture\SortableGroup + targetEntity: Gedmo\Tests\Mapping\Fixture\SortableGroup gedmo: - sortableGroup diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Uploadable.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Uploadable.dcm.yml similarity index 93% rename from tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Uploadable.dcm.yml rename to tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Uploadable.dcm.yml index 67e668d207..410b9c64da 100644 --- a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Uploadable.dcm.yml +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.Uploadable.dcm.yml @@ -1,5 +1,5 @@ --- -Mapping\Fixture\Yaml\Uploadable: +Gedmo\Tests\Mapping\Fixture\Yaml\Uploadable: type: entity table: uploadables gedmo: diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.User.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.User.dcm.yml similarity index 75% rename from tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.User.dcm.yml rename to tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.User.dcm.yml index 6357889bf7..b9f6dfef5f 100644 --- a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.User.dcm.yml +++ b/tests/Gedmo/Mapping/Driver/Yaml/Gedmo.Tests.Mapping.Fixture.Yaml.User.dcm.yml @@ -1,9 +1,9 @@ --- -Mapping\Fixture\Yaml\User: +Gedmo\Tests\Mapping\Fixture\Yaml\User: type: entity gedmo: translation: - entity: Translatable\Fixture\PersonTranslation + entity: Gedmo\Tests\Translatable\Fixture\PersonTranslation locale: localeField table: users id: @@ -27,8 +27,8 @@ Mapping\Fixture\Yaml\User: length: 128 nullable: true gedmo: - translatable: - fallback: true + translatable: + fallback: true indexes: search_idx: columns: username diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Referenced.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Referenced.dcm.yml deleted file mode 100644 index 89951b8f87..0000000000 --- a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Referenced.dcm.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -Mapping\Fixture\Yaml\Referenced: - type: entity - fields: - id: - type: id - id: true - referencer: - reference: true - type: one - targetDocument: Gedmo\Mapping\Fixture\Yaml\Referencer - inversedBy: referencedDocuments \ No newline at end of file diff --git a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Referencer.dcm.yml b/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Referencer.dcm.yml deleted file mode 100644 index 20eeaa7005..0000000000 --- a/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Referencer.dcm.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -Mapping\Fixture\Yaml\Referencer: - type: entity - fields: - id: - type: id - id: true - referencedDocuments: - reference: true - type: many - targetDocument: Gedmo\Mapping\Fixture\Yaml\Referenced - mappedBy: referencer - gedmo: - referenceIntegrity: nullify \ No newline at end of file diff --git a/tests/Gedmo/Mapping/ExtensionODMTest.php b/tests/Gedmo/Mapping/ExtensionODMTest.php index ce959814f5..9af0898d4e 100644 --- a/tests/Gedmo/Mapping/ExtensionODMTest.php +++ b/tests/Gedmo/Mapping/ExtensionODMTest.php @@ -1,24 +1,32 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Gedmo\Mapping\Mock\Extension\Encoder\EncoderListener; use Doctrine\ODM\MongoDB\Event\LoadClassMetadataEventArgs; -use Mapping\Fixture\Document\User; +use Gedmo\Mapping\MappedEventSubscriber; +use Gedmo\Tests\Mapping\Fixture\Document\User; +use Gedmo\Tests\Mapping\Mock\Extension\Encoder\EncoderListener; +use Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping\Event\Adapter\ODM; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; -class ExtensionODMTest extends BaseTestCaseMongoODM +final class ExtensionODMTest extends BaseTestCaseMongoODM { - const USER = 'Mapping\\Fixture\\Document\\User'; + private EncoderListener $encoderListener; - private $encoderListener; - - protected function setUp() + protected function setUp(): void { parent::setUp(); - require_once __DIR__.'/Mock/Extension/Encoder/Mapping/Annotations.php'; $evm = new EventManager(); $this->encoderListener = new EncoderListener(); $evm->addEventSubscriber($this->encoderListener); @@ -26,25 +34,24 @@ protected function setUp() $this->getMockDocumentManager($evm); } - public function testExtensionMetadata() + public function testExtensionMetadata(): void { - $meta = $this->dm->getClassMetadata(self::USER); - $config = $this->encoderListener->getConfiguration($this->dm, self::USER); - $this->assertArrayHasKey('encode', $config); - $this->assertCount(2, $config['encode']); + $config = $this->encoderListener->getConfiguration($this->dm, User::class); + static::assertArrayHasKey('encode', $config); + static::assertCount(2, $config['encode']); - $this->assertArrayHasKey('name', $config['encode']); + static::assertArrayHasKey('name', $config['encode']); $options = $config['encode']['name']; - $this->assertEquals('sha1', $options['type']); - $this->assertEquals('xxx', $options['secret']); + static::assertSame('sha1', $options['type']); + static::assertSame('xxx', $options['secret']); - $this->assertArrayHasKey('password', $config['encode']); + static::assertArrayHasKey('password', $config['encode']); $options = $config['encode']['password']; - $this->assertEquals('md5', $options['type']); - $this->assertEmpty($options['secret']); + static::assertSame('md5', $options['type']); + static::assertEmpty($options['secret']); } - public function testGeneratedValues() + public function testGeneratedValues(): void { $user = new User(); $user->setName('encode me'); @@ -52,24 +59,27 @@ public function testGeneratedValues() $this->dm->persist($user); $this->dm->flush(); - $this->assertEquals('c12fead75b49a41d43804e8229cb049d3b91bf42', $user->getName()); - $this->assertEquals('5ebe2294ecd0e0f08eab7690d2a6ee69', $user->getPassword()); + static::assertSame('c12fead75b49a41d43804e8229cb049d3b91bf42', $user->getName()); + static::assertSame('5ebe2294ecd0e0f08eab7690d2a6ee69', $user->getPassword()); } - public function testEventAdapterUsed() + public function testEventAdapterUsed(): void { - $mappedSubscriberClass = new \ReflectionClass('Gedmo\\Mapping\\MappedEventSubscriber'); + $mappedSubscriberClass = new \ReflectionClass(MappedEventSubscriber::class); $getEventAdapterMethod = $mappedSubscriberClass->getMethod('getEventAdapter'); - $getEventAdapterMethod->setAccessible(true); + + if (PHP_VERSION_ID < 80100) { + $getEventAdapterMethod->setAccessible(true); + } $loadClassMetadataEventArgs = new LoadClassMetadataEventArgs( - $this->dm->getClassMetadata(self::USER), + $this->dm->getClassMetadata(User::class), $this->dm ); $eventAdapter = $getEventAdapterMethod->invoke( $this->encoderListener, $loadClassMetadataEventArgs ); - $this->assertEquals('Gedmo\\Mapping\\Mock\\Extension\\Encoder\\Mapping\\Event\\Adapter\\ODM', get_class($eventAdapter)); + static::assertInstanceOf(ODM::class, $eventAdapter); } } diff --git a/tests/Gedmo/Mapping/ExtensionORMTest.php b/tests/Gedmo/Mapping/ExtensionORMTest.php index b7d89f4e03..d288e2c8c7 100644 --- a/tests/Gedmo/Mapping/ExtensionORMTest.php +++ b/tests/Gedmo/Mapping/ExtensionORMTest.php @@ -1,82 +1,93 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Gedmo\Mapping\Mock\Extension\Encoder\EncoderListener; use Doctrine\ORM\Event\LoadClassMetadataEventArgs; -use Mapping\Fixture\User; +use Gedmo\Mapping\MappedEventSubscriber; +use Gedmo\Tests\Mapping\Fixture\User; +use Gedmo\Tests\Mapping\Mock\Extension\Encoder\EncoderListener; +use Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping\Event\Adapter\ORM; +use Gedmo\Tests\Tool\BaseTestCaseORM; -class ExtensionORMTest extends BaseTestCaseORM +final class ExtensionORMTest extends BaseTestCaseORM { - const USER = 'Mapping\\Fixture\\User'; - - private $encoderListener; + private EncoderListener $encoderListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); - require_once __DIR__.'/Mock/Extension/Encoder/Mapping/Annotations.php'; $evm = new EventManager(); $this->encoderListener = new EncoderListener(); $evm->addEventSubscriber($this->encoderListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testExtensionMetadata() + public function testExtensionMetadata(): void { - $meta = $this->em->getClassMetadata(self::USER); - $config = $this->encoderListener->getConfiguration($this->em, self::USER); - $this->assertArrayHasKey('encode', $config); - $this->assertCount(2, $config['encode']); + $config = $this->encoderListener->getConfiguration($this->em, User::class); + static::assertArrayHasKey('encode', $config); + static::assertCount(2, $config['encode']); - $this->assertArrayHasKey('name', $config['encode']); + static::assertArrayHasKey('name', $config['encode']); $options = $config['encode']['name']; - $this->assertEquals('sha1', $options['type']); - $this->assertEquals('xxx', $options['secret']); + static::assertSame('sha1', $options['type']); + static::assertSame('xxx', $options['secret']); - $this->assertArrayHasKey('password', $config['encode']); + static::assertArrayHasKey('password', $config['encode']); $options = $config['encode']['password']; - $this->assertEquals('md5', $options['type']); - $this->assertEmpty($options['secret']); + static::assertSame('md5', $options['type']); + static::assertEmpty($options['secret']); } - public function testGeneratedValues() + public function testGeneratedValues(): void { $user = new User(); $user->setName('encode me'); $user->setPassword('secret'); + $user->setUsername('some_username'); $this->em->persist($user); $this->em->flush(); - $this->assertEquals('c12fead75b49a41d43804e8229cb049d3b91bf42', $user->getName()); - $this->assertEquals('5ebe2294ecd0e0f08eab7690d2a6ee69', $user->getPassword()); + static::assertSame('c12fead75b49a41d43804e8229cb049d3b91bf42', $user->getName()); + static::assertSame('5ebe2294ecd0e0f08eab7690d2a6ee69', $user->getPassword()); } - public function testEventAdapterUsed() + public function testEventAdapterUsed(): void { - $mappedSubscriberClass = new \ReflectionClass('Gedmo\\Mapping\\MappedEventSubscriber'); + $mappedSubscriberClass = new \ReflectionClass(MappedEventSubscriber::class); $getEventAdapterMethod = $mappedSubscriberClass->getMethod('getEventAdapter'); - $getEventAdapterMethod->setAccessible(true); + + if (PHP_VERSION_ID < 80100) { + $getEventAdapterMethod->setAccessible(true); + } $loadClassMetadataEventArgs = new LoadClassMetadataEventArgs( - $this->em->getClassMetadata(self::USER), + $this->em->getClassMetadata(User::class), $this->em ); $eventAdapter = $getEventAdapterMethod->invoke( $this->encoderListener, $loadClassMetadataEventArgs ); - $this->assertEquals('Gedmo\\Mapping\\Mock\\Extension\\Encoder\\Mapping\\Event\\Adapter\\ORM', get_class($eventAdapter)); + static::assertInstanceOf(ORM::class, $eventAdapter); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::USER, - ); + return [ + User::class, + ]; } } diff --git a/tests/Gedmo/Mapping/Fixture/Annotation/TranslatableModel.php b/tests/Gedmo/Mapping/Fixture/Annotation/TranslatableModel.php new file mode 100644 index 0000000000..7d9bb024b2 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Annotation/TranslatableModel.php @@ -0,0 +1,38 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Annotation; + +use Gedmo\Mapping\Annotation as Gedmo; + +class TranslatableModel +{ + /** + * @var string|null + * + * @Gedmo\Translatable + */ + private $title; + + /** + * @var string|null + * + * @Gedmo\Translatable(fallback=true) + */ + private $titleFallbackTrue; + + /** + * @var string|null + * + * @Gedmo\Translatable(fallback=false) + */ + private $titleFallbackFalse; +} diff --git a/tests/Gedmo/Mapping/Fixture/Annotation/TranslationEntityModel.php b/tests/Gedmo/Mapping/Fixture/Annotation/TranslationEntityModel.php new file mode 100644 index 0000000000..37fb345fa0 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Annotation/TranslationEntityModel.php @@ -0,0 +1,21 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Annotation; + +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @Gedmo\TranslationEntity(class="stdClass") + */ +class TranslationEntityModel +{ +} diff --git a/tests/Gedmo/Mapping/Fixture/Attribute/TranslatableModel.php b/tests/Gedmo/Mapping/Fixture/Attribute/TranslatableModel.php new file mode 100644 index 0000000000..d864aab74d --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Attribute/TranslatableModel.php @@ -0,0 +1,26 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Attribute; + +use Gedmo\Mapping\Annotation as Gedmo; + +class TranslatableModel +{ + #[Gedmo\Translatable] + private ?string $title = null; + + #[Gedmo\Translatable(fallback: true)] + private ?string $titleFallbackTrue = null; + + #[Gedmo\Translatable(fallback: false)] + private ?string $titleFallbackFalse = null; +} diff --git a/tests/Gedmo/Mapping/Fixture/Attribute/TranslationEntityModel.php b/tests/Gedmo/Mapping/Fixture/Attribute/TranslationEntityModel.php new file mode 100644 index 0000000000..a528fc2a62 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Attribute/TranslationEntityModel.php @@ -0,0 +1,19 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Attribute; + +use Gedmo\Mapping\Annotation as Gedmo; + +#[Gedmo\TranslationEntity(class: \stdClass::class)] +class TranslationEntityModel +{ +} diff --git a/tests/Gedmo/Mapping/Fixture/BaseCategory.php b/tests/Gedmo/Mapping/Fixture/BaseCategory.php new file mode 100644 index 0000000000..b39c012029 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/BaseCategory.php @@ -0,0 +1,148 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\MappedSuperclass + */ +#[ORM\MappedSuperclass] +class BaseCategory +{ + /** + * @ORM\Column(type="integer") + * + * @Gedmo\TreeLeft + */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private ?int $left = null; + + /** + * @ORM\Column(type="integer") + * + * @Gedmo\TreeRight + */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeRight] + private ?int $right = null; + + /** + * @ORM\Column(type="integer") + * + * @Gedmo\TreeLevel + */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private ?int $level = null; + + /** + * @ORM\Column(type="integer") + * + * @Gedmo\TreeRoot + */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeRoot] + private ?int $rooted = null; + + /** + * @ORM\Column(type="datetime") + * + * @Gedmo\Timestampable(on="create") + */ + #[ORM\Column(type: Types::DATETIME_MUTABLE)] + #[Gedmo\Timestampable(on: 'create')] + private ?\DateTime $created = null; + + /** + * @ORM\Column(type="date") + * + * @Gedmo\Timestampable(on="update") + */ + #[ORM\Column(type: Types::DATE_MUTABLE)] + #[Gedmo\Timestampable(on: 'update')] + private ?\DateTime $updated = null; + + public function setCreated(\DateTime $created): void + { + $this->created = $created; + } + + /** + * @return \DateTime $created + */ + public function getCreated(): ?\DateTime + { + return $this->created; + } + + public function setUpdated(\DateTime $updated): void + { + $this->updated = $updated; + } + + public function getUpdated(): ?\DateTime + { + return $this->updated; + } + + public function setLeft(int $left): self + { + $this->left = $left; + + return $this; + } + + public function getLeft(): int + { + return $this->left; + } + + public function setRight(int $right): self + { + $this->right = $right; + + return $this; + } + + public function getRight(): int + { + return $this->right; + } + + public function setLevel(int $level): self + { + $this->level = $level; + + return $this; + } + + public function getLevel(): int + { + return $this->level; + } + + public function setRooted(int $rooted): self + { + $this->rooted = $rooted; + + return $this; + } + + public function getRooted(): int + { + return $this->rooted; + } +} diff --git a/tests/Gedmo/Mapping/Fixture/Category.php b/tests/Gedmo/Mapping/Fixture/Category.php new file mode 100644 index 0000000000..84b49edb98 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Category.php @@ -0,0 +1,175 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Entity\LogEntry; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\RelativeSlugHandler; +use Gedmo\Sluggable\Handler\TreeSlugHandler; +use Gedmo\Tests\Translatable\Fixture\CategoryTranslation; + +/** + * @ORM\Entity + * @ORM\Table(name="categories") + * + * @Gedmo\Loggable(logEntryClass="Gedmo\Loggable\Entity\LogEntry") + * @Gedmo\TranslationEntity(class="Gedmo\Tests\Translatable\Fixture\CategoryTranslation") + * @Gedmo\Tree(type="nested") + */ +#[ORM\Entity] +#[ORM\Table(name: 'categories')] +#[Gedmo\Loggable(logEntryClass: LogEntry::class)] +#[Gedmo\TranslationEntity(class: CategoryTranslation::class)] +#[Gedmo\Tree(type: 'nested')] +class Category extends BaseCategory +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(type="string", length=64) + * + * @Gedmo\Translatable + */ + #[ORM\Column(type: Types::STRING, length: 64)] + #[Gedmo\Translatable] + private ?string $title = null; + + /** + * @ORM\Column(type="string", length=64) + * + * @Gedmo\Slug( + * fields={"title"}, + * style="camel", + * separator="_", + * handlers={ + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="relationField", value="parent"), + * @Gedmo\SlugHandlerOption(name="relationSlugField", value="parent"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }), + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }) + * } + * ) + */ + #[ORM\Column(type: Types::STRING, length: 64)] + #[Gedmo\Slug(fields: ['title'], style: 'camel', separator: '_')] + #[Gedmo\SlugHandler(class: RelativeSlugHandler::class, options: ['relationField' => 'parent', 'relationSlugField' => 'slug', 'separator' => '/'])] + #[Gedmo\SlugHandler(class: TreeSlugHandler::class, options: ['parentRelationField' => 'parent', 'separator' => '/'])] + private ?string $slug = null; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="Gedmo\Tests\Mapping\Fixture\Category", mappedBy="parent") + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private $children; + + /** + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Mapping\Fixture\Category", inversedBy="children") + * + * @Gedmo\TreeParent + */ + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[Gedmo\TreeParent] + private ?Category $parent = null; + + /** + * @var \DateTime + * + * @ORM\Column(type="date") + * + * @Gedmo\Timestampable(on="change", field="title", value="Test") + */ + #[ORM\Column(type: Types::DATE_MUTABLE)] + #[Gedmo\Timestampable(on: 'change', field: 'title', value: 'Test')] + private $changed; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + + /** + * @return int $id + */ + public function getId(): int + { + return $this->id; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setSlug(string $slug): void + { + $this->slug = $slug; + } + + /** + * @return string $slug + */ + public function getSlug(): string + { + return $this->slug; + } + + public function addChildren(self $children): void + { + $this->children[] = $children; + } + + /** + * @return Collection $children + */ + public function getChildren() + { + return $this->children; + } + + public function setParent(self $parent): void + { + $this->parent = $parent; + } + + /** + * @return self $parent + */ + public function getParent(): self + { + return $this->parent; + } +} diff --git a/tests/Gedmo/Mapping/Fixture/ClosureTreeClosure.php b/tests/Gedmo/Mapping/Fixture/ClosureTreeClosure.php index 1b81ae1756..e5911946a8 100644 --- a/tests/Gedmo/Mapping/Fixture/ClosureTreeClosure.php +++ b/tests/Gedmo/Mapping/Fixture/ClosureTreeClosure.php @@ -1,13 +1,47 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; -use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tests\Mapping\Fixture\Xml\ClosureTree; +use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; /** * @ORM\Entity + * @ORM\Table( + * indexes={@ORM\Index(name="closure_tree_depth_idx", columns={"depth"})}, + * uniqueConstraints={@ORM\UniqueConstraint(name="closure_tree_unique_idx", columns={ + * "ancestor", "descendant" + * })} + * ) */ +#[ORM\Entity] +#[ORM\UniqueConstraint(name: 'closure_tree_unique_idx', columns: ['ancestor', 'descendant'])] +#[ORM\Index(name: 'closure_tree_depth_idx', columns: ['depth'])] class ClosureTreeClosure extends AbstractClosure { + /** + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Mapping\Fixture\Xml\ClosureTree") + * @ORM\JoinColumn(name="ancestor", referencedColumnName="id", nullable=false, onDelete="CASCADE") + */ + #[ORM\ManyToOne(targetEntity: ClosureTree::class)] + #[ORM\JoinColumn(name: 'ancestor', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected $ancestor; + + /** + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Mapping\Fixture\Xml\ClosureTree") + * @ORM\JoinColumn(name="descendant", referencedColumnName="id", nullable=false, onDelete="CASCADE") + */ + #[ORM\ManyToOne(targetEntity: ClosureTree::class)] + #[ORM\JoinColumn(name: 'descendant', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected $descendant; } diff --git a/tests/Gedmo/Mapping/Fixture/Compatibility/Article.php b/tests/Gedmo/Mapping/Fixture/Compatibility/Article.php deleted file mode 100644 index 615fe04b1a..0000000000 --- a/tests/Gedmo/Mapping/Fixture/Compatibility/Article.php +++ /dev/null @@ -1,77 +0,0 @@ -id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - /** - * Get created - * - * @return datetime $created - */ - public function getCreated() - { - return $this->created; - } - - public function setCreated(\DateTime $created) - { - $this->created = $created; - } - - /** - * Get updated - * - * @return datetime $updated - */ - public function getUpdated() - { - return $this->updated; - } - - public function setUpdated(\DateTime $updated) - { - $this->updated = $updated; - } -} diff --git a/tests/Gedmo/Mapping/Fixture/Document/User.php b/tests/Gedmo/Mapping/Fixture/Document/User.php index 36db1838cd..420bbb9889 100644 --- a/tests/Gedmo/Mapping/Fixture/Document/User.php +++ b/tests/Gedmo/Mapping/Fixture/Document/User.php @@ -1,48 +1,68 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Document; -use Gedmo\Mapping\Mock\Extension\Encoder\Mapping as Ext; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; +use Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping as Ext; /** * @ODM\Document(collection="test_users") */ +#[ODM\Document(collection: 'test_users')] class User { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** * @Ext\Encode(type="sha1", secret="xxx") - * @ODM\String + * + * @ODM\Field(type="string") */ - private $name; + #[Ext\Encode(type: 'sha1', secret: 'xxx')] + #[ODM\Field(type: Type::STRING)] + private ?string $name = null; /** * @Ext\Encode(type="md5") - * @ODM\String + * + * @ODM\Field(type="string") */ - private $password; + #[Ext\Encode(type: 'md5')] + #[ODM\Field(type: Type::STRING)] + private ?string $password = null; - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setPassword($password) + public function setPassword(?string $password): void { $this->password = $password; } - public function getPassword() + public function getPassword(): ?string { return $this->password; } diff --git a/tests/Gedmo/Mapping/Fixture/Embedded.php b/tests/Gedmo/Mapping/Fixture/Embedded.php new file mode 100644 index 0000000000..13f607680f --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Embedded.php @@ -0,0 +1,34 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +/** + * Class Embedded + * + * @author Fabian Sabau + * + * @ORM\Embeddable + */ +#[ORM\Embeddable] +class Embedded +{ + /** + * @var string + * + * @ORM\Column(type="string") + */ + #[ORM\Column(type: Types::STRING)] + private $subtitle; +} diff --git a/tests/Gedmo/Mapping/Fixture/Loggable.php b/tests/Gedmo/Mapping/Fixture/Loggable.php new file mode 100644 index 0000000000..ddc27024d5 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Loggable.php @@ -0,0 +1,61 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Entity\LogEntry; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * + * @Gedmo\Loggable(logEntryClass="Gedmo\Loggable\Entity\LogEntry") + */ +#[ORM\Entity] +#[Gedmo\Loggable(logEntryClass: LogEntry::class)] +class Loggable +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + * + * @Gedmo\Versioned + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + #[Gedmo\Versioned] + private ?string $title = null; + + public function getId(): int + { + return $this->id; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/tests/Gedmo/Mapping/Fixture/LoggableComposite.php b/tests/Gedmo/Mapping/Fixture/LoggableComposite.php new file mode 100644 index 0000000000..9af9f2116c --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/LoggableComposite.php @@ -0,0 +1,74 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Entity\LogEntry; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * + * @Gedmo\Loggable(logEntryClass="Gedmo\Loggable\Entity\LogEntry") + */ +#[ORM\Entity] +#[Gedmo\Loggable(logEntryClass: LogEntry::class)] +class LoggableComposite +{ + /** + * @var int + * + * @ORM\Id + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + private $one; + + /** + * @var int + * + * @ORM\Id + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + private $two; + + /** + * @ORM\Column(name="title", type="string", length=64) + * + * @Gedmo\Versioned + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + #[Gedmo\Versioned] + private ?string $title = null; + + public function getOne(): int + { + return $this->one; + } + + public function getTwo(): int + { + return $this->two; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/tests/Gedmo/Mapping/Fixture/LoggableCompositeRelation.php b/tests/Gedmo/Mapping/Fixture/LoggableCompositeRelation.php new file mode 100644 index 0000000000..6244fe9f5b --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/LoggableCompositeRelation.php @@ -0,0 +1,74 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Entity\LogEntry; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * + * @Gedmo\Loggable(logEntryClass="Gedmo\Loggable\Entity\LogEntry") + */ +#[ORM\Entity] +#[Gedmo\Loggable(logEntryClass: LogEntry::class)] +class LoggableCompositeRelation +{ + /** + * @var Loggable + * + * @ORM\Id + * @ORM\ManyToOne(targetEntity="Loggable") + */ + #[ORM\Id] + #[ORM\ManyToOne(targetEntity: Loggable::class)] + private $one; + + /** + * @var int + * + * @ORM\Id + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + private $two; + + /** + * @ORM\Column(name="title", type="string", length=64) + * + * @Gedmo\Versioned + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + #[Gedmo\Versioned] + private ?string $title = null; + + public function getOne(): Loggable + { + return $this->one; + } + + public function getTwo(): int + { + return $this->two; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/tests/Gedmo/Mapping/Fixture/LoggableWithEmbedded.php b/tests/Gedmo/Mapping/Fixture/LoggableWithEmbedded.php new file mode 100644 index 0000000000..77c36f7a75 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/LoggableWithEmbedded.php @@ -0,0 +1,61 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Loggable\Entity\LogEntry; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * + * @Gedmo\Loggable(logEntryClass="Gedmo\Loggable\Entity\LogEntry") + */ +#[ORM\Entity] +#[Gedmo\Loggable(logEntryClass: LogEntry::class)] +class LoggableWithEmbedded +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @var string + * + * @ORM\Column(name="title", type="string") + * + * @Gedmo\Versioned + */ + #[ORM\Column(name: 'title', type: Types::STRING)] + #[Gedmo\Versioned] + private $title; + + /** + * @var Embedded + * + * @ORM\Embedded(class="Gedmo\Tests\Mapping\Fixture\Embedded") + * + * @Gedmo\Versioned + */ + #[ORM\Embedded(class: Embedded::class)] + #[Gedmo\Versioned] + private $embedded; +} diff --git a/tests/Gedmo/Mapping/Fixture/MappedSuperClass.php b/tests/Gedmo/Mapping/Fixture/MappedSuperClass.php new file mode 100644 index 0000000000..60592871d4 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/MappedSuperClass.php @@ -0,0 +1,54 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping as Ext; + +/** + * @ORM\MappedSuperclass + */ +#[ORM\MappedSuperclass] +class MappedSuperClass +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(length=32) + * + * @Ext\Encode(type="md5") + */ + #[Ext\Encode(type: 'md5')] + #[ORM\Column(length: 32)] + private ?string $content = null; + + public function setContent(?string $content): void + { + $this->content = $content; + } + + public function getContent(): ?string + { + return $this->content; + } +} diff --git a/tests/Gedmo/Mapping/Fixture/Sluggable.php b/tests/Gedmo/Mapping/Fixture/Sluggable.php index 8f790c2980..707d27acc8 100644 --- a/tests/Gedmo/Mapping/Fixture/Sluggable.php +++ b/tests/Gedmo/Mapping/Fixture/Sluggable.php @@ -1,84 +1,123 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\RelativeSlugHandler; +use Gedmo\Sluggable\Handler\TreeSlugHandler; /** * @ORM\Entity */ +#[ORM\Entity] class Sluggable { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @ORM\Column(name="code", type="string", length=16) */ - private $code; + #[ORM\Column(name: 'code', type: Types::STRING, length: 16, nullable: true)] + private ?string $code = null; /** + * @ORM\Column(name="ean", type="string", length=13) + */ + #[ORM\Column(name: 'ean', type: Types::STRING, length: 13, nullable: true)] + private ?string $ean = null; + + /** + * @var string|null + * * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }), - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="relationField", value="user"), - * @Gedmo\SlugHandlerOption(name="relationSlugField", value="slug"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }) - * }, separator="-", updatable=false, fields={"title", "code"}) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }), + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="relationField", value="parent"), + * @Gedmo\SlugHandlerOption(name="relationSlugField", value="test"), + * @Gedmo\SlugHandlerOption(name="separator", value="-") + * }) + * }, separator="_", updatable=false, fields={"title", "ean", "code"}, style="camel") + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ + #[Gedmo\Slug(separator: '_', updatable: false, fields: ['title', 'ean', 'code'], style: 'camel')] + #[Gedmo\SlugHandler(class: TreeSlugHandler::class, options: ['parentRelationField' => 'parent', 'separator' => '/'])] + #[Gedmo\SlugHandler(class: RelativeSlugHandler::class, options: ['relationField' => 'parent', 'relationSlugField' => 'test', 'separator' => '-'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] private $slug; /** + * @var Sluggable|null + * * @ORM\ManyToOne(targetEntity="Sluggable") */ + #[ORM\ManyToOne(targetEntity: self::class)] private $parent; /** - * @ORM\ManyToOne(targetEntity="User") - */ + * @var User|null + * + * @ORM\ManyToOne(targetEntity="User") + */ + #[ORM\ManyToOne(targetEntity: User::class)] private $user; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Mapping/Fixture/SoftDeleteable.php b/tests/Gedmo/Mapping/Fixture/SoftDeleteable.php index 02fb7619fd..3e3372e786 100644 --- a/tests/Gedmo/Mapping/Fixture/SoftDeleteable.php +++ b/tests/Gedmo/Mapping/Fixture/SoftDeleteable.php @@ -1,54 +1,84 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class SoftDeleteable { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; + private ?string $title = null; + + private ?string $code = null; + + /** + * @var string|null + */ + private $slug; + /** + * @var \DateTime|null + * * @ORM\Column(name="deleted_at", type="datetime", nullable=true) */ + #[ORM\Column(name: 'deleted_at', type: Types::DATETIME_MUTABLE, nullable: true)] private $deletedAt; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Mapping/Fixture/Sortable.php b/tests/Gedmo/Mapping/Fixture/Sortable.php new file mode 100644 index 0000000000..ca03ce2c61 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Sortable.php @@ -0,0 +1,103 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * @ORM\Table(name="sortables") + */ +#[ORM\Entity] +#[ORM\Table(name: 'sortables')] +class Sortable +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @var string + * + * @ORM\Column(type="string", length=128) + */ + #[ORM\Column(type: Types::STRING, length: 128)] + private $title; + + /** + * @var int + * + * @ORM\Column(type="integer") + * + * @Gedmo\SortablePosition + */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\SortablePosition] + private $position; + + /** + * @var string + * + * @ORM\Column(type="string", length=128) + * + * @Gedmo\SortableGroup + */ + #[ORM\Column(type: Types::STRING, length: 128)] + #[Gedmo\SortableGroup] + private $grouping; + + /** + * @var SortableGroup + * + * @ORM\ManyToOne(targetEntity="Sluggable") + * + * @Gedmo\SortableGroup + */ + #[ORM\ManyToOne(targetEntity: SortableGroup::class)] + #[Gedmo\SortableGroup] + private $sortable_group; + + /** + * @var Collection + * + * @ORM\ManyToMany(targetEntity="SortableGroup") + * @ORM\JoinTable(name="sortable_sortable_groups", + * joinColumns={@ORM\JoinColumn(name="sortable_id")}, + * inverseJoinColumns={@ORM\JoinColumn(name="group_id")} + * ) + * + * @Gedmo\SortableGroup + */ + #[ORM\ManyToMany(targetEntity: SortableGroup::class)] + #[ORM\JoinTable(name: 'sortable_sortable_groups')] + #[ORM\JoinColumn(name: 'sortable_id')] + #[ORM\InverseJoinColumn(name: 'group_id')] + #[Gedmo\SortableGroup] + private Collection $sortable_groups; + + public function __construct() + { + $this->sortable_groups = new ArrayCollection(); + } +} diff --git a/tests/Gedmo/Mapping/Fixture/SortableGroup.php b/tests/Gedmo/Mapping/Fixture/SortableGroup.php index 57be14bd8b..ff5e8485b0 100644 --- a/tests/Gedmo/Mapping/Fixture/SortableGroup.php +++ b/tests/Gedmo/Mapping/Fixture/SortableGroup.php @@ -1,24 +1,44 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** - * @ORM\Table(name="test_sortable_groups") * @ORM\Entity + * @ORM\Table(name="test_sortable_groups") */ +#[ORM\Entity] +#[ORM\Table(name: 'test_sortable_groups')] class SortableGroup { /** - * @ORM\Column(type="integer") + * @var int + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** - * @ORM\Column(length=64) + * @var string + * + * @ORM\Column(type="string", length=64) */ + #[ORM\Column(type: Types::STRING, length: 64)] private $name; } diff --git a/tests/Gedmo/Mapping/Fixture/SuperClassExtension.php b/tests/Gedmo/Mapping/Fixture/SuperClassExtension.php new file mode 100644 index 0000000000..ec8d3a1445 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/SuperClassExtension.php @@ -0,0 +1,37 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class SuperClassExtension extends MappedSuperClass +{ + /** + * @ORM\Column(length=128) + */ + #[ORM\Column(length: 128)] + private ?string $title = null; + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } +} diff --git a/tests/Gedmo/Mapping/Fixture/Unmapped/Timestampable.php b/tests/Gedmo/Mapping/Fixture/Unmapped/Timestampable.php index 7f98619d4a..08f921f5db 100644 --- a/tests/Gedmo/Mapping/Fixture/Unmapped/Timestampable.php +++ b/tests/Gedmo/Mapping/Fixture/Unmapped/Timestampable.php @@ -1,15 +1,30 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Unmapped; use Gedmo\Mapping\Annotation\Timestampable as Tmsp; class Timestampable { + /** + * @var int + */ private $id; /** + * @var \DateTime + * * @Tmsp(on="create") */ + #[Tmsp(on: 'create')] private $created; } diff --git a/tests/Gedmo/Mapping/Fixture/Uploadable.php b/tests/Gedmo/Mapping/Fixture/Uploadable.php new file mode 100644 index 0000000000..118ffe8ff2 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Uploadable.php @@ -0,0 +1,86 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Uploadable\Mapping\Validator; + +/** + * @ORM\Entity + * + * @Gedmo\Uploadable(allowOverwrite=true, appendNumber=true, path="/my/path", pathMethod="getPath", callback="callbackMethod", filenameGenerator="SHA1", maxSize="1500", allowedTypes="text/plain,text/css", disallowedTypes="video/jpeg,text/html") + */ +#[ORM\Entity] +#[Gedmo\Uploadable(allowOverwrite: true, appendNumber: true, path: '/my/path', pathMethod: 'getPath', callback: 'callbackMethod', filenameGenerator: Validator::FILENAME_GENERATOR_SHA1, maxSize: '1500', allowedTypes: 'text/plain,text/css', disallowedTypes: 'video/jpeg,text/html')] +class Uploadable +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @var string + * + * @ORM\Column(name="mime", type="string") + * + * @Gedmo\UploadableFileMimeType + */ + #[ORM\Column(name: 'mime', type: Types::STRING)] + #[Gedmo\UploadableFileMimeType] + private $mimeType; + + /** + * @var array + */ + private $fileInfo; + + /** + * @var float + * + * @ORM\Column(name="size", type="decimal", precision=10, scale=2) + * + * @Gedmo\UploadableFileSize + */ + #[ORM\Column(name: 'size', type: Types::DECIMAL, precision: 10, scale: 2)] + #[Gedmo\UploadableFileSize] + private $size; + + /** + * @var string + * + * @ORM\Column(name="path", type="string") + * + * @Gedmo\UploadableFilePath + */ + #[ORM\Column(name: 'path', type: Types::STRING)] + #[Gedmo\UploadableFilePath] + private $path; + + public function getPath(): string + { + return $this->path; + } + + public function callbackMethod(): void + { + } +} diff --git a/tests/Gedmo/Mapping/Fixture/User.php b/tests/Gedmo/Mapping/Fixture/User.php index 575ff47dd5..9a5a6058cd 100644 --- a/tests/Gedmo/Mapping/Fixture/User.php +++ b/tests/Gedmo/Mapping/Fixture/User.php @@ -1,52 +1,134 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Gedmo\Mapping\Mock\Extension\Encoder\Mapping as Ext; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping as Ext; +use Gedmo\Tests\Translatable\Fixture\PersonTranslation; /** - * @ORM\Table(name="test_users") + * @ORM\Table(name="users") + * @ORM\Table( + * name="users", + * indexes={@ORM\Index(name="search_idx", columns={"username"})} + * ) * @ORM\Entity + * + * @Gedmo\TranslationEntity(class="Gedmo\Tests\Translatable\Fixture\PersonTranslation") */ +#[ORM\Table(name: 'users')] +#[ORM\Entity] +#[ORM\Index(columns: ['username'], name: 'search_idx')] +#[Gedmo\TranslationEntity(class: PersonTranslation::class)] class User { /** - * @ORM\Column(type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Ext\Encode(type="sha1", secret="xxx") + * * @ORM\Column(length=64) */ - private $name; + #[Ext\Encode(type: 'sha1', secret: 'xxx')] + #[ORM\Column(length: 64)] + private ?string $name = null; /** * @Ext\Encode(type="md5") + * * @ORM\Column(length=32) + * + * @Gedmo\Translatable + */ + #[Ext\Encode(type: 'md5')] + #[ORM\Column(length: 32)] + #[Gedmo\Translatable] + private ?string $password = null; + + /** + * @ORM\Column(length=128) + * + * @Gedmo\Translatable + */ + #[ORM\Column(length: 128)] + #[Gedmo\Translatable] + private ?string $username = null; + + /** + * @ORM\Column(length=128, nullable=true) + * + * @Gedmo\Translatable(fallback=true) + */ + #[ORM\Column(length: 128, nullable: true)] + #[Gedmo\Translatable(fallback: true)] + private ?string $company = null; + + /** + * @var string + * + * @Gedmo\Locale */ - private $password; + #[Gedmo\Locale] + private $localeField; - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setPassword($password) + public function setPassword(?string $password): void { $this->password = $password; } - public function getPassword() + public function getPassword(): ?string { return $this->password; } + + public function setUsername(string $username): void + { + $this->username = $username; + } + + public function getUsername(): string + { + return $this->username; + } + + public function setCompany(string $company): void + { + $this->company = $company; + } + + public function getCompany(): string + { + return $this->company; + } } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/ClosureTree.php b/tests/Gedmo/Mapping/Fixture/Xml/ClosureTree.php index e65c5b901c..7f5ff9af87 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/ClosureTree.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/ClosureTree.php @@ -1,14 +1,35 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class ClosureTree { + /** + * @var int + */ private $id; + /** + * @var string + */ private $name; + /** + * @var ClosureTree|null + */ private $parent; + /** + * @var int + */ private $level; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/Embedded.php b/tests/Gedmo/Mapping/Fixture/Xml/Embedded.php index 4ed6142f1f..6da360a93b 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/Embedded.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/Embedded.php @@ -1,13 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; /** * Class Embedded - * @package Mapping\Fixture\Xml + * * @author Fabian Sabau */ class Embedded { + /** + * @var string + */ private $subtitle; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/EmbeddedTranslatable.php b/tests/Gedmo/Mapping/Fixture/Xml/EmbeddedTranslatable.php index 6d103d85d3..f499fee56c 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/EmbeddedTranslatable.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/EmbeddedTranslatable.php @@ -1,8 +1,20 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class EmbeddedTranslatable { + /** + * @var string + */ private $subtitle; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/Loggable.php b/tests/Gedmo/Mapping/Fixture/Xml/Loggable.php index 7caef986ce..4d99010e1b 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/Loggable.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/Loggable.php @@ -1,12 +1,30 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class Loggable { + /** + * @var int + */ private $id; + /** + * @var string + */ private $title; + /** + * @var Status + */ private $status; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/LoggableComposite.php b/tests/Gedmo/Mapping/Fixture/Xml/LoggableComposite.php new file mode 100644 index 0000000000..59ca1eadfd --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Xml/LoggableComposite.php @@ -0,0 +1,28 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; + +class LoggableComposite +{ + /** + * @var int + */ + private $one; + + /** + * @var int + */ + private $two; + + /** + * @var string + */ + private $title; +} diff --git a/tests/Gedmo/Mapping/Fixture/Xml/LoggableCompositeRelation.php b/tests/Gedmo/Mapping/Fixture/Xml/LoggableCompositeRelation.php new file mode 100644 index 0000000000..0db9ba164f --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Xml/LoggableCompositeRelation.php @@ -0,0 +1,28 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; + +class LoggableCompositeRelation +{ + /** + * @var Loggable + */ + private $one; + + /** + * @var int + */ + private $two; + + /** + * @var string + */ + private $title; +} diff --git a/tests/Gedmo/Mapping/Fixture/Xml/LoggableWithEmbedded.php b/tests/Gedmo/Mapping/Fixture/Xml/LoggableWithEmbedded.php index d432a7fdc3..7d1626ac81 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/LoggableWithEmbedded.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/LoggableWithEmbedded.php @@ -1,14 +1,35 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class LoggableWithEmbedded { + /** + * @var int + */ private $id; + /** + * @var string + */ private $title; + /** + * @var Status + */ private $status; + /** + * @var Embedded + */ private $embedded; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/MaterializedPathTree.php b/tests/Gedmo/Mapping/Fixture/Xml/MaterializedPathTree.php index 96873f9ede..1a3d4e0b42 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/MaterializedPathTree.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/MaterializedPathTree.php @@ -1,18 +1,50 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class MaterializedPathTree { + /** + * @var int + */ private $id; + /** + * @var string + */ private $title; + /** + * @var string + */ private $path; + /** + * @var \DateTime|null + */ private $lockTime; + /** + * @var string + */ + private $pathHash; + + /** + * @var MaterializedPathTree + */ private $parent; + /** + * @var int + */ private $level; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/NestedTree.php b/tests/Gedmo/Mapping/Fixture/Xml/NestedTree.php index d00049b57f..70baa2832d 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/NestedTree.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/NestedTree.php @@ -1,20 +1,50 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class NestedTree { + /** + * @var int + */ private $id; + /** + * @var string + */ private $name; + /** + * @var NestedTree + */ private $parent; + /** + * @var int + */ private $root; + /** + * @var int + */ private $level; + /** + * @var int + */ private $left; + /** + * @var int + */ private $right; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/References.php b/tests/Gedmo/Mapping/Fixture/Xml/References.php new file mode 100644 index 0000000000..d18e62393c --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Xml/References.php @@ -0,0 +1,32 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; + +use Gedmo\Tests\Mapping\Fixture\Document\User; + +class References +{ + /** + * @var int + */ + private $id; + + /** + * @var string + */ + private $name; + + /** + * @var User[] + */ + private $users; +} diff --git a/tests/Gedmo/Mapping/Fixture/Xml/Sluggable.php b/tests/Gedmo/Mapping/Fixture/Xml/Sluggable.php index ded3e19cb6..834b3f47e1 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/Sluggable.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/Sluggable.php @@ -1,18 +1,45 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class Sluggable { + /** + * @var int + */ private $id; + /** + * @var string + */ private $title; + /** + * @var string + */ private $code; + /** + * @var string + */ private $ean; + /** + * @var string + */ private $slug; + /** + * @var Sluggable + */ private $parent; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/SoftDeleteable.php b/tests/Gedmo/Mapping/Fixture/Xml/SoftDeleteable.php index 04d96496c2..9f1f6aa707 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/SoftDeleteable.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/SoftDeleteable.php @@ -1,10 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class SoftDeleteable { + /** + * @var int + */ private $id; + /** + * @var \DateTime|null + */ private $deletedAt; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/Sortable.php b/tests/Gedmo/Mapping/Fixture/Xml/Sortable.php index 2d264d6e4d..42a51ad463 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/Sortable.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/Sortable.php @@ -1,18 +1,48 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; + +use Doctrine\Common\Collections\Collection; +use Gedmo\Tests\Mapping\Fixture\SortableGroup; class Sortable { + /** + * @var int + */ private $id; + /** + * @var string + */ private $title; + /** + * @var int + */ private $position; + /** + * @var string + */ private $grouping; + /** + * @var SortableGroup + */ private $sortable_group; + /** + * @var Collection + */ private $sortable_groups; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/Status.php b/tests/Gedmo/Mapping/Fixture/Xml/Status.php index d43e2a5a29..3407058bf7 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/Status.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/Status.php @@ -1,10 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class Status { + /** + * @var int + */ private $id; + /** + * @var string + */ private $title; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/Timestampable.php b/tests/Gedmo/Mapping/Fixture/Xml/Timestampable.php index 60adb062fd..e7587c37ce 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/Timestampable.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/Timestampable.php @@ -1,16 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class Timestampable { + /** + * @var int + */ private $id; + /** + * @var \DateTime + */ private $created; + /** + * @var \DateTime + */ private $updated; + /** + * @var \DateTime + */ private $published; + /** + * @var Status + */ private $status; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/Translatable.php b/tests/Gedmo/Mapping/Fixture/Xml/Translatable.php index 1daebbb501..c4f1c3d936 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/Translatable.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/Translatable.php @@ -1,18 +1,45 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class Translatable { + /** + * @var int + */ private $id; + /** + * @var string + */ private $title; + /** + * @var string + */ private $content; + /** + * @var string + */ private $locale; + /** + * @var string + */ private $author; + /** + * @var int + */ private $views; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/TranslatableWithEmbedded.php b/tests/Gedmo/Mapping/Fixture/Xml/TranslatableWithEmbedded.php index 71c05af2bc..18c717e57d 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/TranslatableWithEmbedded.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/TranslatableWithEmbedded.php @@ -1,20 +1,50 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class TranslatableWithEmbedded { + /** + * @var int + */ private $id; + /** + * @var string + */ private $title; + /** + * @var string + */ private $content; + /** + * @var string + */ private $locale; + /** + * @var string + */ private $author; + /** + * @var int + */ private $views; + /** + * @var EmbeddedTranslatable + */ private $embedded; } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/Uploadable.php b/tests/Gedmo/Mapping/Fixture/Xml/Uploadable.php index 4aec6bb524..25774ea290 100644 --- a/tests/Gedmo/Mapping/Fixture/Xml/Uploadable.php +++ b/tests/Gedmo/Mapping/Fixture/Xml/Uploadable.php @@ -1,25 +1,49 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; class Uploadable { + /** + * @var int + */ private $id; + /** + * @var string + */ private $mimeType; + /** + * @var array + */ private $fileInfo; + /** + * @var float + */ private $size; + /** + * @var string + */ private $path; - public function getPath() + public function getPath(): string { return $this->path; } - public function callbackMethod() + public function callbackMethod(): void { } } diff --git a/tests/Gedmo/Mapping/Fixture/Xml/User.php b/tests/Gedmo/Mapping/Fixture/Xml/User.php new file mode 100644 index 0000000000..9972cc2844 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Xml/User.php @@ -0,0 +1,66 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Xml; + +class User +{ + /** + * @var int + */ + private $id; + + private ?string $password = null; + + private ?string $username = null; + + private ?string $company = null; + + /** + * @var string + */ + private $localeField; + + public function getId(): int + { + return $this->id; + } + + public function setPassword(string $password): void + { + $this->password = $password; + } + + public function getPassword(): string + { + return $this->password; + } + + public function setUsername(string $username): void + { + $this->username = $username; + } + + public function getUsername(): string + { + return $this->username; + } + + public function setCompany(string $company): void + { + $this->company = $company; + } + + public function getCompany(): string + { + return $this->company; + } +} diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/BaseCategory.php b/tests/Gedmo/Mapping/Fixture/Yaml/BaseCategory.php index c1be7c0f65..becc8bd0e0 100644 --- a/tests/Gedmo/Mapping/Fixture/Yaml/BaseCategory.php +++ b/tests/Gedmo/Mapping/Fixture/Yaml/BaseCategory.php @@ -1,126 +1,97 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + class BaseCategory { - /** - * @Column(type="integer") - */ - private $left; + private ?int $left = null; - /** - * @Column(type="integer") - */ - private $right; + private ?int $right = null; - /** - * @Column(type="integer") - */ - private $level; + private ?int $level = null; - /** - * @Column(type="integer") - */ - private $rooted; + private ?int $rooted = null; - /** - * @var datetime $created - * - * @Column(name="created", type="datetime") - */ - private $created; + private ?\DateTime $created = null; - /** - * @var date $updated - * - * @Column(name="updated", type="date") - */ - private $updated; + private ?\DateTime $updated = null; - /** - * Set created - * - * @param dateTime $created - */ - public function setCreated(\dateTime $created) + public function setCreated(\DateTime $created): void { $this->created = $created; } /** - * Get created - * - * @return dateTime $created + * @return \DateTime $created */ - public function getCreated() + public function getCreated(): ?\DateTime { return $this->created; } - /** - * Set updated - * - * @param date $updated - */ - public function setUpdated($updated) + public function setUpdated(\DateTime $updated): void { $this->updated = $updated; } - /** - * Get updated - * - * @return date $updated - */ - public function getUpdated() + public function getUpdated(): ?\DateTime { return $this->updated; } - public function setLeft($left) + public function setLeft(int $left): self { $this->left = $left; + return $this; } - public function getLeft() + public function getLeft(): int { return $this->left; } - public function setRight($right) + public function setRight(int $right): self { $this->right = $right; + return $this; } - public function getRight() + public function getRight(): int { return $this->right; } - public function setLevel($level) + public function setLevel(int $level): self { $this->level = $level; + return $this; } - public function getLevel() + public function getLevel(): int { return $this->level; } - public function setRooted($rooted) + public function setRooted(int $rooted): self { $this->rooted = $rooted; + return $this; } - public function getRooted() + public function getRooted(): int { return $this->rooted; } diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/Category.php b/tests/Gedmo/Mapping/Fixture/Yaml/Category.php index 0879c4e1b5..c92156f94d 100644 --- a/tests/Gedmo/Mapping/Fixture/Yaml/Category.php +++ b/tests/Gedmo/Mapping/Fixture/Yaml/Category.php @@ -1,142 +1,100 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; + class Category extends BaseCategory { /** - * @var integer $id - * - * @Column(name="id", type="integer") - * @Id - * @GeneratedValue(strategy="IDENTITY") + * @var int */ private $id; - /** - * @var string $title - * - * @Column(name="title", type="string", length=64) - */ - private $title; + private ?string $title = null; - /** - * @var string $slug - * - * @Column(name="slug", type="string", length=64) - */ - private $slug; + private ?string $slug = null; /** - * @var Entity\Category - * - * @OneToMany(targetEntity="Category", mappedBy="parent") + * @var Collection */ private $children; + private ?Category $parent = null; + /** - * @var Entity\Category - * - * @ManyToOne(targetEntity="Category", inversedBy="children") - * @JoinColumns({ - * @JoinColumn(name="parent_id", referencedColumnName="id") - * }) + * @var \DateTime */ - private $parent; - private $changed; + public function __construct() + { + $this->children = new ArrayCollection(); + } + /** - * Get id - * - * @return integer $id + * @return int $id */ - public function getId() + public function getId(): int { return $this->id; } - /** - * Set title - * - * @param string $title - */ - public function setTitle($title) + public function setTitle(string $title): void { $this->title = $title; } - /** - * Get title - * - * @return string $title - */ - public function getTitle() + public function getTitle(): string { return $this->title; } - /** - * Set slug - * - * @param string $slug - */ - public function setSlug($slug) + public function setSlug(string $slug): void { $this->slug = $slug; } /** - * Get slug - * * @return string $slug */ - public function getSlug() + public function getSlug(): string { return $this->slug; } - /** - * Add children - * - * @param Entity\Category $children - */ - public function addChildren(Category $children) + public function addChildren(self $children): void { $this->children[] = $children; } /** - * Get children - * - * @return Doctrine\Common\Collections\Collection $children + * @return Collection $children */ public function getChildren() { return $this->children; } - /** - * Set parent - * - * @param Entity\Category $parent - */ - public function setParent($parent) + public function setParent(self $parent): void { $this->parent = $parent; } /** - * Get parent - * - * @return Entity\Category $parent + * @return self $parent */ - public function getParent() + public function getParent(): self { return $this->parent; } diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/ClosureCategory.php b/tests/Gedmo/Mapping/Fixture/Yaml/ClosureCategory.php index 6b2b1f24c3..2225d2d1bb 100644 --- a/tests/Gedmo/Mapping/Fixture/Yaml/ClosureCategory.php +++ b/tests/Gedmo/Mapping/Fixture/Yaml/ClosureCategory.php @@ -1,95 +1,86 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; class ClosureCategory { + /** + * @var int + */ private $id; - private $title; + private ?string $title = null; + /** + * @var Collection + */ private $children; - private $parent; + private ?ClosureCategory $parent = null; - private $level; + private ?int $level = null; - /** - * Get id - * - * @return integer $id - */ - public function getId() + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getId(): int { return $this->id; } - /** - * Set title - * - * @param string $title - */ - public function setTitle($title) + public function setTitle(string $title): void { $this->title = $title; } - /** - * Get title - * - * @return string $title - */ - public function getTitle() + public function getTitle(): string { return $this->title; } - /** - * Add children - * - * @param Entity\Category $children - */ - public function addChildren(Category $children) + public function addChildren(self $children): void { $this->children[] = $children; } /** - * Get children - * - * @return Doctrine\Common\Collections\Collection $children + * @return Collection */ - public function getChildren() + public function getChildren(): Collection { return $this->children; } - /** - * Set parent - * - * @param Entity\Category $parent - */ - public function setParent($parent) + public function setParent(self $parent): void { $this->parent = $parent; } - /** - * Get parent - * - * @return Entity\Category $parent - */ - public function getParent() + public function getParent(): self { return $this->parent; } - public function setLevel($level) + public function setLevel(int $level): void { $this->level = $level; } - public function getLevel() + public function getLevel(): int { return $this->level; } diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/Embedded.php b/tests/Gedmo/Mapping/Fixture/Yaml/Embedded.php new file mode 100644 index 0000000000..ff4c78b136 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Yaml/Embedded.php @@ -0,0 +1,25 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + +/** + * Class Embedded + * + * @author Fabian Sabau + */ +class Embedded +{ + /** + * @var string + */ + private $subtitle; +} diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/Loggable.php b/tests/Gedmo/Mapping/Fixture/Yaml/Loggable.php new file mode 100644 index 0000000000..7c4dcb506f --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Yaml/Loggable.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + +class Loggable +{ + /** + * @var int + */ + private $id; + + /** + * @var string + */ + private $title; +} diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/LoggableComposite.php b/tests/Gedmo/Mapping/Fixture/Yaml/LoggableComposite.php new file mode 100644 index 0000000000..81036f0126 --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Yaml/LoggableComposite.php @@ -0,0 +1,28 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + +class LoggableComposite +{ + /** + * @var int + */ + private $one; + + /** + * @var int + */ + private $two; + + /** + * @var string + */ + private $title; +} diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/LoggableCompositeRelation.php b/tests/Gedmo/Mapping/Fixture/Yaml/LoggableCompositeRelation.php new file mode 100644 index 0000000000..713708663d --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Yaml/LoggableCompositeRelation.php @@ -0,0 +1,28 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + +class LoggableCompositeRelation +{ + /** + * @var Loggable + */ + private $one; + + /** + * @var int + */ + private $two; + + /** + * @var string + */ + private $title; +} diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/LoggableWithEmbedded.php b/tests/Gedmo/Mapping/Fixture/Yaml/LoggableWithEmbedded.php new file mode 100644 index 0000000000..bd93e8efce --- /dev/null +++ b/tests/Gedmo/Mapping/Fixture/Yaml/LoggableWithEmbedded.php @@ -0,0 +1,30 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + +class LoggableWithEmbedded +{ + /** + * @var int + */ + private $id; + + /** + * @var string + */ + private $title; + + /** + * @var Embedded + */ + private $embedded; +} diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/MaterializedPathCategory.php b/tests/Gedmo/Mapping/Fixture/Yaml/MaterializedPathCategory.php index 4e7e4834db..67992e89b4 100644 --- a/tests/Gedmo/Mapping/Fixture/Yaml/MaterializedPathCategory.php +++ b/tests/Gedmo/Mapping/Fixture/Yaml/MaterializedPathCategory.php @@ -1,119 +1,110 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; class MaterializedPathCategory { + /** + * @var int + */ private $id; - private $title; + private ?string $title = null; - private $path; + private ?string $path = null; - private $level; + private ?int $level = null; + /** + * @var Collection + */ private $children; - private $parent; + private ?MaterializedPathCategory $parent = null; - private $lockTime; + private ?\DateTime $lockTime = null; - /** - * Get id - * - * @return integer $id - */ - public function getId() + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getId(): int { return $this->id; } - /** - * Set title - * - * @param string $title - */ - public function setTitle($title) + public function setTitle(string $title): void { $this->title = $title; } - /** - * Get title - * - * @return string $title - */ - public function getTitle() + public function getTitle(): string { return $this->title; } - /** - * Add children - * - * @param Entity\Category $children - */ - public function addChildren(Category $children) + public function addChildren(Category $children): void { $this->children[] = $children; } /** - * Get children - * - * @return Doctrine\Common\Collections\Collection $children + * @return Collection */ - public function getChildren() + public function getChildren(): Collection { return $this->children; } - /** - * Set parent - * - * @param Entity\Category $parent - */ - public function setParent($parent) + public function setParent(self $parent): void { $this->parent = $parent; } - /** - * Get parent - * - * @return Entity\Category $parent - */ - public function getParent() + public function getParent(): self { return $this->parent; } - public function setLevel($level) + public function setLevel(?int $level): void { $this->level = $level; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } - public function setPath($path) + public function setPath(?string $path): void { $this->path = $path; } - public function getPath() + public function getPath(): string { return $this->path; } - public function setLockTime($lockTime) + public function setLockTime(?\DateTime $lockTime): void { $this->lockTime = $lockTime; } - public function getLockTime() + public function getLockTime(): ?\DateTime { return $this->lockTime; } diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/Referenced.php b/tests/Gedmo/Mapping/Fixture/Yaml/Referenced.php index a1a0ac19e2..0e5eb55448 100644 --- a/tests/Gedmo/Mapping/Fixture/Yaml/Referenced.php +++ b/tests/Gedmo/Mapping/Fixture/Yaml/Referenced.php @@ -1,10 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; class Referenced { + /** + * @var int + */ private $id; + /** + * @var Referencer + */ private $referencer; -} \ No newline at end of file +} diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/Referencer.php b/tests/Gedmo/Mapping/Fixture/Yaml/Referencer.php index 1d02b681c8..ba00640f8d 100644 --- a/tests/Gedmo/Mapping/Fixture/Yaml/Referencer.php +++ b/tests/Gedmo/Mapping/Fixture/Yaml/Referencer.php @@ -1,10 +1,27 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + +use Doctrine\Common\Collections\Collection; class Referencer { + /** + * @var int + */ private $id; + /** + * @var Collection + */ private $referencedDocuments; -} \ No newline at end of file +} diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/SoftDeleteable.php b/tests/Gedmo/Mapping/Fixture/Yaml/SoftDeleteable.php index 3f7a3dc45d..188aa0f9ca 100644 --- a/tests/Gedmo/Mapping/Fixture/Yaml/SoftDeleteable.php +++ b/tests/Gedmo/Mapping/Fixture/Yaml/SoftDeleteable.php @@ -1,10 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; class SoftDeleteable { + /** + * @var int + */ private $id; + /** + * @var \DateTime|null + */ private $deletedAt; } diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/Sortable.php b/tests/Gedmo/Mapping/Fixture/Yaml/Sortable.php index 22a1c9dfff..94eb70e21c 100644 --- a/tests/Gedmo/Mapping/Fixture/Yaml/Sortable.php +++ b/tests/Gedmo/Mapping/Fixture/Yaml/Sortable.php @@ -1,18 +1,48 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; + +use Doctrine\Common\Collections\Collection; +use Gedmo\Tests\Mapping\Fixture\SortableGroup; class Sortable { + /** + * @var int + */ private $id; + /** + * @var string + */ private $title; + /** + * @var int + */ private $position; + /** + * @var string + */ private $grouping; + /** + * @var SortableGroup + */ private $sortable_group; + /** + * @var Collection + */ private $sortable_groups; } diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/Uploadable.php b/tests/Gedmo/Mapping/Fixture/Yaml/Uploadable.php index 6af112db78..d5d1ccc8e7 100644 --- a/tests/Gedmo/Mapping/Fixture/Yaml/Uploadable.php +++ b/tests/Gedmo/Mapping/Fixture/Yaml/Uploadable.php @@ -1,25 +1,49 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; class Uploadable { + /** + * @var int + */ private $id; + /** + * @var string + */ private $mimeType; + /** + * @var array + */ private $fileInfo; + /** + * @var float + */ private $size; + /** + * @var string + */ private $path; - public function getPath() + public function getPath(): string { return $this->path; } - public function callbackMethod() + public function callbackMethod(): void { } } diff --git a/tests/Gedmo/Mapping/Fixture/Yaml/User.php b/tests/Gedmo/Mapping/Fixture/Yaml/User.php index 3dcd456051..95e0f426cd 100644 --- a/tests/Gedmo/Mapping/Fixture/Yaml/User.php +++ b/tests/Gedmo/Mapping/Fixture/Yaml/User.php @@ -1,84 +1,65 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Fixture\Yaml; class User { + /** + * @var int + */ private $id; - private $password; + private ?string $password = null; - private $username; + private ?string $username = null; - private $company; + private ?string $company = null; - private $localeField; /** - * Get id - * - * @return integer $id + * @var string */ - public function getId() + private $localeField; + + public function getId(): int { return $this->id; } - /** - * Set password - * - * @param string $password - */ - public function setPassword($password) + public function setPassword(string $password): void { $this->password = $password; } - /** - * Get password - * - * @return string $password - */ - public function getPassword() + public function getPassword(): string { return $this->password; } - /** - * Set username - * - * @param string $username - */ - public function setUsername($username) + public function setUsername(string $username): void { $this->username = $username; } - /** - * Get username - * - * @return string $username - */ - public function getUsername() + public function getUsername(): string { return $this->username; } - /** - * Set company - * - * @param $company - */ - public function setCompany($company) + public function setCompany(string $company): void { $this->company = $company; } - /** - * Get company - * - * @return string $company - */ - public function getCompany() + public function getCompany(): string { return $this->company; } diff --git a/tests/Gedmo/Mapping/LoggableMappingTest.php b/tests/Gedmo/Mapping/LoggableMappingTest.php deleted file mode 100644 index 7a05a4e9d9..0000000000 --- a/tests/Gedmo/Mapping/LoggableMappingTest.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class LoggableMappingTest extends \PHPUnit_Framework_TestCase -{ - const YAML_CATEGORY = 'Mapping\Fixture\Yaml\Category'; - private $em; - - public function setUp() - { - $config = new \Doctrine\ORM\Configuration(); - $config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); - $config->setQueryCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); - $config->setProxyDir(TESTS_TEMP_DIR); - $config->setProxyNamespace('Gedmo\Mapping\Proxy'); - $chainDriverImpl = new DriverChain(); - $chainDriverImpl->addDriver( - new YamlDriver(array(__DIR__.'/Driver/Yaml')), - 'Mapping\Fixture\Yaml' - ); - $config->setMetadataDriverImpl($chainDriverImpl); - - $conn = array( - 'driver' => 'pdo_sqlite', - 'memory' => true, - ); - - //$config->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger()); - - $evm = new \Doctrine\Common\EventManager(); - $evm->addEventSubscriber(new LoggableListener()); - $this->em = \Doctrine\ORM\EntityManager::create($conn, $config, $evm); - } - - public function testLoggableMapping() - { - $meta = $this->em->getClassMetadata(self::YAML_CATEGORY); - $cacheId = ExtensionMetadataFactory::getCacheId(self::YAML_CATEGORY, 'Gedmo\Loggable'); - $config = $this->em->getMetadataFactory()->getCacheDriver()->fetch($cacheId); - - $this->assertArrayHasKey('loggable', $config); - $this->assertTrue($config['loggable']); - $this->assertArrayHasKey('logEntryClass', $config); - $this->assertEquals('Gedmo\\Loggable\\Entity\\LogEntry', $config['logEntryClass']); - } -} diff --git a/tests/Gedmo/Mapping/LoggableORMMappingTest.php b/tests/Gedmo/Mapping/LoggableORMMappingTest.php new file mode 100644 index 0000000000..f2d07801e2 --- /dev/null +++ b/tests/Gedmo/Mapping/LoggableORMMappingTest.php @@ -0,0 +1,282 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\YamlDriver; +use Gedmo\Loggable\Entity\LogEntry; +use Gedmo\Loggable\LoggableListener; +use Gedmo\Mapping\ExtensionMetadataFactory; +use Gedmo\Tests\Mapping\Fixture\Loggable as AnnotatedLoggable; +use Gedmo\Tests\Mapping\Fixture\LoggableComposite as AnnotatedLoggableComposite; +use Gedmo\Tests\Mapping\Fixture\LoggableCompositeRelation as AnnotatedLoggableCompositeRelation; +use Gedmo\Tests\Mapping\Fixture\LoggableWithEmbedded as AnnotatedLoggableWithEmbedded; +use Gedmo\Tests\Mapping\Fixture\Xml\Loggable as XmlLoggable; +use Gedmo\Tests\Mapping\Fixture\Xml\LoggableComposite as XmlLoggableComposite; +use Gedmo\Tests\Mapping\Fixture\Xml\LoggableCompositeRelation as XmlLoggableCompositeRelation; +use Gedmo\Tests\Mapping\Fixture\Xml\LoggableWithEmbedded as XmlLoggableWithEmbedded; +use Gedmo\Tests\Mapping\Fixture\Yaml\Loggable as YamlLoggable; +use Gedmo\Tests\Mapping\Fixture\Yaml\LoggableComposite as YamlLoggableComposite; +use Gedmo\Tests\Mapping\Fixture\Yaml\LoggableCompositeRelation as YamlLoggableCompositeRelation; +use Gedmo\Tests\Mapping\Fixture\Yaml\LoggableWithEmbedded as YamlLoggableWithEmbedded; + +/** + * These are mapping tests for the loggable extension + * + * @author Gediminas Morkevicius + */ +final class LoggableORMMappingTest extends ORMMappingTestCase +{ + private EntityManager $em; + + protected function setUp(): void + { + parent::setUp(); + + $listener = new LoggableListener(); + $listener->setCacheItemPool($this->cache); + + $this->em = $this->getBasicEntityManager(); + $this->em->getEventManager()->addEventSubscriber($listener); + } + + /** + * @return \Generator + * + * @note the XML fixture has a different mapping from the other configs, so it is tested separately + */ + public static function dataLoggableObject(): \Generator + { + if (PHP_VERSION_ID >= 80000) { + yield 'Model with attributes' => [AnnotatedLoggable::class]; + } elseif (class_exists(AnnotationDriver::class)) { + yield 'Model with annotations' => [AnnotatedLoggable::class]; + } + + if (class_exists(YamlDriver::class)) { + yield 'Model with YAML mapping' => [YamlLoggable::class]; + } + } + + /** + * @param class-string $className + * + * @dataProvider dataLoggableObject + */ + public function testLoggableMapping(string $className): void + { + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Loggable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('logEntryClass', $config); + static::assertSame(LogEntry::class, $config['logEntryClass']); + static::assertArrayHasKey('loggable', $config); + static::assertTrue($config['loggable']); + + static::assertArrayHasKey('versioned', $config); + static::assertCount(1, $config['versioned']); + static::assertContains('title', $config['versioned']); + } + + public function testLoggableXmlMapping(): void + { + $className = XmlLoggable::class; + + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Loggable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('logEntryClass', $config); + static::assertSame(LogEntry::class, $config['logEntryClass']); + static::assertArrayHasKey('loggable', $config); + static::assertTrue($config['loggable']); + + static::assertArrayHasKey('versioned', $config); + static::assertCount(2, $config['versioned']); + static::assertContains('title', $config['versioned']); + static::assertContains('status', $config['versioned']); + } + + /** + * @return \Generator + */ + public static function dataLoggableObjectWithCompositeKey(): \Generator + { + yield 'Model with XML mapping' => [XmlLoggableComposite::class]; + + if (PHP_VERSION_ID >= 80000) { + yield 'Model with attributes' => [AnnotatedLoggableComposite::class]; + } elseif (class_exists(AnnotationDriver::class)) { + yield 'Model with annotations' => [AnnotatedLoggableComposite::class]; + } + + if (class_exists(YamlDriver::class)) { + yield 'Model with YAML mapping' => [YamlLoggableComposite::class]; + } + } + + /** + * @param class-string $className + * + * @dataProvider dataLoggableObjectWithCompositeKey + */ + public function testLoggableCompositeMapping(string $className): void + { + $meta = $this->em->getClassMetadata($className); + + static::assertIsArray($meta->identifier); + static::assertCount(2, $meta->identifier); + + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Loggable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('loggable', $config); + static::assertTrue($config['loggable']); + static::assertArrayHasKey('logEntryClass', $config); + static::assertSame(LogEntry::class, $config['logEntryClass']); + + static::assertArrayHasKey('versioned', $config); + static::assertCount(1, $config['versioned']); + static::assertContains('title', $config['versioned']); + } + + /** + * @return \Generator + */ + public static function dataLoggableObjectWithCompositeKeyAndRelation(): \Generator + { + yield 'Model with XML mapping' => [XmlLoggableCompositeRelation::class]; + + if (PHP_VERSION_ID >= 80000) { + yield 'Model with attributes' => [AnnotatedLoggableCompositeRelation::class]; + } elseif (class_exists(AnnotationDriver::class)) { + yield 'Model with annotations' => [AnnotatedLoggableCompositeRelation::class]; + } + + if (class_exists(YamlDriver::class)) { + yield 'Model with YAML mapping' => [YamlLoggableCompositeRelation::class]; + } + } + + /** + * @param class-string $className + * + * @dataProvider dataLoggableObjectWithCompositeKeyAndRelation + */ + public function testLoggableCompositeRelationMapping(string $className): void + { + $meta = $this->em->getClassMetadata($className); + + static::assertIsArray($meta->identifier); + static::assertCount(2, $meta->identifier); + + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Loggable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('loggable', $config); + static::assertTrue($config['loggable']); + static::assertArrayHasKey('logEntryClass', $config); + static::assertSame(LogEntry::class, $config['logEntryClass']); + + static::assertArrayHasKey('versioned', $config); + static::assertCount(1, $config['versioned']); + static::assertContains('title', $config['versioned']); + } + + /* + * Each of the mapping drivers handles versioning embedded objects differently, so instead of using a single test case, + * these will be run as separate cases checking each driver's config appropriately. + */ + + /** + * @return \Generator + */ + public static function dataLoggableObjectWithEmbedded(): \Generator + { + if (PHP_VERSION_ID >= 80000) { + yield 'Model with attributes' => [AnnotatedLoggableWithEmbedded::class]; + } elseif (class_exists(AnnotationDriver::class)) { + yield 'Model with annotations' => [AnnotatedLoggableWithEmbedded::class]; + } + } + + /** + * @param class-string $className + * + * @dataProvider dataLoggableObjectWithEmbedded + */ + public function testLoggableAnnotatedWithEmbedded(string $className): void + { + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Loggable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('logEntryClass', $config); + static::assertSame(LogEntry::class, $config['logEntryClass']); + static::assertArrayHasKey('loggable', $config); + static::assertTrue($config['loggable']); + + static::assertArrayHasKey('versioned', $config); + static::assertCount(1, $config['versioned']); + static::assertContains('title', $config['versioned']); + } + + public function testLoggableXmlWithEmbedded(): void + { + $className = XmlLoggableWithEmbedded::class; + + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Loggable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('logEntryClass', $config); + static::assertSame(LogEntry::class, $config['logEntryClass']); + static::assertArrayHasKey('loggable', $config); + static::assertTrue($config['loggable']); + + static::assertArrayHasKey('versioned', $config); + static::assertCount(3, $config['versioned']); + static::assertContains('title', $config['versioned']); + static::assertContains('status', $config['versioned']); + static::assertContains('embedded', $config['versioned']); + } + + public function testLoggableYamlWithEmbedded(): void + { + if (!class_exists(YamlDriver::class)) { + static::markTestSkipped('Test case requires the deprecated YAML mapping driver from the ORM.'); + } + + $className = YamlLoggableWithEmbedded::class; + + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Loggable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('logEntryClass', $config); + static::assertSame(LogEntry::class, $config['logEntryClass']); + static::assertArrayHasKey('loggable', $config); + static::assertTrue($config['loggable']); + + static::assertArrayHasKey('versioned', $config); + static::assertCount(2, $config['versioned']); + static::assertContains('title', $config['versioned']); + static::assertContains('embedded.subtitle', $config['versioned']); + } +} diff --git a/tests/Gedmo/Mapping/MappingEventAdapterTest.php b/tests/Gedmo/Mapping/MappingEventAdapterTest.php index 5d45bac02f..8b5a0eba28 100644 --- a/tests/Gedmo/Mapping/MappingEventAdapterTest.php +++ b/tests/Gedmo/Mapping/MappingEventAdapterTest.php @@ -1,55 +1,57 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\PrePersistEventArgs; use Gedmo\Mapping\Event\Adapter\ORM as EventAdapterORM; -use Doctrine\ORM\Event\LifecycleEventArgs; -use Gedmo\Mapping\Mock\Mapping\Event\Adapter\ORM as CustomizedORMAdapter; +use Gedmo\Tests\Mapping\Mock\EventSubscriberCustomMock; +use Gedmo\Tests\Mapping\Mock\EventSubscriberMock; +use Gedmo\Tests\Mapping\Mock\Mapping\Event\Adapter\ORM as CustomizedORMAdapter; +use PHPUnit\Framework\TestCase; -class MappingEventAdapterTest extends \PHPUnit_Framework_TestCase +final class MappingEventAdapterTest extends TestCase { - public function testCustomizedAdapter() + public function testCustomizedAdapter(): void { - $emMock = $this->getMockBuilder('Doctrine\\ORM\\EntityManager') - ->disableOriginalConstructor() - ->getMock(); $subscriber = new EventSubscriberCustomMock(); - $args = new LifecycleEventArgs(new \stdClass(), $emMock); + $args = new PrePersistEventArgs(new \stdClass(), static::createStub(EntityManagerInterface::class)); $adapter = $subscriber->getAdapter($args); - $this->assertTrue($adapter instanceof CustomizedORMAdapter); + static::assertInstanceOf(CustomizedORMAdapter::class, $adapter); } - public function testCorrectAdapter() + public function testCorrectAdapter(): void { - $emMock = $this->getMockBuilder('Doctrine\\ORM\\EntityManager') - ->disableOriginalConstructor() - ->getMock(); + $emMock = static::createStub(EntityManagerInterface::class); $subscriber = new EventSubscriberMock(); - $args = new LifecycleEventArgs(new \stdClass(), $emMock); + $args = new PrePersistEventArgs(new \stdClass(), $emMock); $adapter = $subscriber->getAdapter($args); - $this->assertTrue($adapter instanceof EventAdapterORM); - $this->assertTrue($adapter->getObjectManager() === $emMock); - $this->assertTrue($adapter->getObject() instanceof \stdClass); + static::assertInstanceOf(EventAdapterORM::class, $adapter); + static::assertSame($adapter->getObjectManager(), $emMock); + static::assertInstanceOf(\stdClass::class, $adapter->getObject()); } - public function testAdapterBehavior() + public function testAdapterBehavior(): void { - $eventArgsMock = $this->getMockBuilder('Doctrine\\ORM\\Event\\LifecycleEventArgs') - ->disableOriginalConstructor() - ->getMock(); - $eventArgsMock->expects($this->once()) - ->method('getEntityManager'); + $emMock = static::createStub(EntityManagerInterface::class); + $entity = new \stdClass(); - $eventArgsMock->expects($this->once()) - ->method('getEntity'); + $args = new PrePersistEventArgs($entity, $emMock); $eventAdapter = new EventAdapterORM(); - $eventAdapter->setEventArgs($eventArgsMock); - $eventAdapter->getObjectManager(); - $eventAdapter->getObject(); + $eventAdapter->setEventArgs($args); + static::assertSame($eventAdapter->getObjectManager(), $emMock); + static::assertInstanceOf(\stdClass::class, $eventAdapter->getObject()); } } diff --git a/tests/Gedmo/Mapping/MappingEventSubscriberTest.php b/tests/Gedmo/Mapping/MappingEventSubscriberTest.php new file mode 100644 index 0000000000..adbe71f8d6 --- /dev/null +++ b/tests/Gedmo/Mapping/MappingEventSubscriberTest.php @@ -0,0 +1,107 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; +use Gedmo\Mapping\ExtensionMetadataFactory; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Mapping\Fixture\Sluggable; +use Gedmo\Tests\Mapping\Fixture\SuperClassExtension; +use Gedmo\Tests\Mapping\Mock\Extension\Encoder\EncoderListener; +use Psr\Cache\CacheItemPoolInterface; + +final class MappingEventSubscriberTest extends ORMMappingTestCase +{ + private EntityManager $em; + + protected function setUp(): void + { + parent::setUp(); + + $config = $this->getBasicConfiguration(); + + if (PHP_VERSION_ID >= 80000) { + $config->setMetadataDriverImpl(new AttributeDriver([])); + } else { + $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader())); + } + + $this->em = $this->getBasicEntityManager($config); + } + + public function testGetMetadataFactoryCacheFromDoctrineForSluggable(): void + { + $metadataFactory = $this->em->getMetadataFactory(); + $getCache = \Closure::bind(static fn (AbstractClassMetadataFactory $metadataFactory): ?CacheItemPoolInterface => $metadataFactory->getCache(), null, \get_class($metadataFactory)); + + $cache = $getCache($metadataFactory); + + $cacheKey = ExtensionMetadataFactory::getCacheId(Sluggable::class, 'Gedmo\Sluggable'); + + static::assertFalse($cache->hasItem($cacheKey)); + + $subscriber = new SluggableListener(); + $classMetadata = $this->em->getClassMetadata(Sluggable::class); + $subscriber->getExtensionMetadataFactory($this->em)->getExtensionMetadata($classMetadata); + + static::assertTrue($cache->hasItem($cacheKey)); + } + + public function testGetMetadataFactoryCacheFromDoctrineForSuperClassExtension(): void + { + $metadataFactory = $this->em->getMetadataFactory(); + $getCache = \Closure::bind(static fn (AbstractClassMetadataFactory $metadataFactory): ?CacheItemPoolInterface => $metadataFactory->getCache(), null, \get_class($metadataFactory)); + + /** @var CacheItemPoolInterface $cache */ + $cache = $getCache($metadataFactory); + + $cacheKey = ExtensionMetadataFactory::getCacheId(SuperClassExtension::class, 'Gedmo\Tests\Mapping\Mock\Extension\Encoder'); + + static::assertFalse($cache->hasItem($cacheKey)); + + $subscriber = new EncoderListener(); + $classMetadata = $this->em->getClassMetadata(SuperClassExtension::class); + + $config = $subscriber->getExtensionMetadataFactory($this->em)->getExtensionMetadata($classMetadata); + + static::assertSame([ + 'content' => [ + 'type' => 'md5', + 'secret' => null, + ], + ], $config['encode']); + + // Create new configuration to use new array cache + $config = $this->getBasicConfiguration(); + + if (PHP_VERSION_ID >= 80000) { + $config->setMetadataDriverImpl(new AttributeDriver([])); + } else { + $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader())); + } + + $this->em = $this->getBasicEntityManager($config); + + $config = $subscriber->getExtensionMetadataFactory($this->em)->getExtensionMetadata($classMetadata); + + static::assertSame([ + 'content' => [ + 'type' => 'md5', + 'secret' => null, + ], + ], $config['encode']); + } +} diff --git a/tests/Gedmo/Mapping/MappingTest.php b/tests/Gedmo/Mapping/MappingTest.php index 874aadb4a3..dcc152fcc2 100644 --- a/tests/Gedmo/Mapping/MappingTest.php +++ b/tests/Gedmo/Mapping/MappingTest.php @@ -1,54 +1,82 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\Common\EventManager; +use Doctrine\DBAL\DriverManager; +use Doctrine\ORM\Configuration; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\Tools\SchemaTool; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Tree\Fixture\BehavioralCategory; +use Gedmo\Timestampable\TimestampableListener; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; +use Gedmo\Tree\TreeListener; +use PHPUnit\Framework\TestCase; /** * These are mapping extension tests * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MappingTest extends \PHPUnit_Framework_TestCase +final class MappingTest extends TestCase { - const TEST_ENTITY_CATEGORY = "Tree\Fixture\BehavioralCategory"; - const TEST_ENTITY_TRANSLATION = "Gedmo\Translatable\Entity\Translation"; + private EntityManager $em; - private $em; - private $timestampable; + private TimestampableListener $timestampable; - public function setUp() + protected function setUp(): void { - $config = new \Doctrine\ORM\Configuration(); - $config->setProxyDir(TESTS_TEMP_DIR); - $config->setProxyNamespace('Gedmo\Mapping\Proxy'); - //$this->markTestSkipped('Skipping according to a bug in annotation reader creation.'); - $config->setMetadataDriverImpl(new \Doctrine\ORM\Mapping\Driver\AnnotationDriver($_ENV['annotation_reader'])); + $config = new Configuration(); - $conn = array( + /** @phpstan-ignore-next-line function.alreadyNarrowedType */ + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'enableNativeLazyObjects')) { + $config->enableNativeLazyObjects(true); + } else { + $config->setProxyDir(TESTS_TEMP_DIR); + $config->setProxyNamespace('Gedmo\Mapping\Proxy'); + } + + if (PHP_VERSION_ID >= 80000) { + $config->setMetadataDriverImpl(new AttributeDriver([])); + } else { + $config->setMetadataDriverImpl(new AnnotationDriver($_ENV['annotation_reader'])); + } + + $conn = [ 'driver' => 'pdo_sqlite', 'memory' => true, - ); + ]; - $evm = new \Doctrine\Common\EventManager(); - $evm->addEventSubscriber(new \Gedmo\Translatable\TranslatableListener()); - $this->timestampable = new \Gedmo\Timestampable\TimestampableListener(); + $evm = new EventManager(); + $evm->addEventSubscriber(new TranslatableListener()); + $this->timestampable = new TimestampableListener(); $evm->addEventSubscriber($this->timestampable); - $evm->addEventSubscriber(new \Gedmo\Sluggable\SluggableListener()); - $evm->addEventSubscriber(new \Gedmo\Tree\TreeListener()); - $this->em = \Doctrine\ORM\EntityManager::create($conn, $config, $evm); + $evm->addEventSubscriber(new SluggableListener()); + $evm->addEventSubscriber(new TreeListener()); + $this->em = new EntityManager(DriverManager::getConnection($conn, $config), $config, $evm); - $schemaTool = new \Doctrine\ORM\Tools\SchemaTool($this->em); - $schemaTool->dropSchema(array()); - $schemaTool->createSchema(array( - $this->em->getClassMetadata(self::TEST_ENTITY_CATEGORY), - $this->em->getClassMetadata(self::TEST_ENTITY_TRANSLATION), - )); + $schemaTool = new SchemaTool($this->em); + $schemaTool->dropSchema([]); + $schemaTool->createSchema([ + $this->em->getClassMetadata(BehavioralCategory::class), + $this->em->getClassMetadata(Translation::class), + ]); } - public function testNoCacheImplementationMapping() + public function testNoCacheImplementationMapping(): void { $food = new BehavioralCategory(); $food->setTitle('Food'); @@ -57,8 +85,8 @@ public function testNoCacheImplementationMapping() // assertion checks if configuration is read correctly without cache driver $conf = $this->timestampable->getConfiguration( $this->em, - self::TEST_ENTITY_CATEGORY + BehavioralCategory::class ); - $this->assertCount(0, $conf); + static::assertCount(0, $conf); } } diff --git a/tests/Gedmo/Mapping/MetadataFactory/CustomDriverTest.php b/tests/Gedmo/Mapping/MetadataFactory/CustomDriverTest.php index cd41b1069a..47fea572c2 100644 --- a/tests/Gedmo/Mapping/MetadataFactory/CustomDriverTest.php +++ b/tests/Gedmo/Mapping/MetadataFactory/CustomDriverTest.php @@ -1,79 +1,112 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\MetadataFactory; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\EventManager; +use Doctrine\DBAL\DriverManager; +use Doctrine\ORM\Configuration; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; +use Gedmo\Mapping\Driver\AttributeReader; +use Gedmo\Tests\Mapping\Fixture\Unmapped\Timestampable; +use Gedmo\Timestampable\TimestampableListener; +use PHPUnit\Framework\TestCase; /** -* These are mapping tests for tree extension -* -* @author Gediminas Morkevicius -* @link http://www.gediminasm.org -* @license MIT License (http://www.opensource.org/licenses/mit-license.php) -*/ -class CustomDriverTest extends \PHPUnit_Framework_TestCase + * These are mapping tests for tree extension + * + * @author Gediminas Morkevicius + */ +final class CustomDriverTest extends TestCase { - public function setUp() + private TimestampableListener $timestampable; + + private EntityManagerInterface $em; + + protected function setUp(): void { - $config = new \Doctrine\ORM\Configuration(); - $config->setProxyDir(TESTS_TEMP_DIR); - $config->setProxyNamespace('Gedmo\Mapping\Proxy'); + $config = new Configuration(); $config->setMetadataDriverImpl(new CustomDriver()); - $conn = array( + /** @phpstan-ignore-next-line function.alreadyNarrowedType */ + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'enableNativeLazyObjects')) { + $config->enableNativeLazyObjects(true); + } else { + $config->setProxyDir(TESTS_TEMP_DIR); + $config->setProxyNamespace('Gedmo\Mapping\Proxy'); + } + + $conn = [ 'driver' => 'pdo_sqlite', 'memory' => true, - ); + ]; + + $evm = new EventManager(); + $this->timestampable = new TimestampableListener(); + + if (PHP_VERSION >= 80000) { + $this->timestampable->setAnnotationReader(new AttributeReader()); + } elseif (class_exists(AnnotationReader::class)) { + $this->timestampable->setAnnotationReader($_ENV['annotation_reader']); + } - $evm = new \Doctrine\Common\EventManager(); - $this->timestampable = new \Gedmo\Timestampable\TimestampableListener(); - $this->timestampable->setAnnotationReader($_ENV['annotation_reader']); $evm->addEventSubscriber($this->timestampable); - $this->em = \Doctrine\ORM\EntityManager::create($conn, $config, $evm); + $connection = DriverManager::getConnection($conn, $config); + $this->em = new EntityManager($connection, $config, $evm); - $schemaTool = new \Doctrine\ORM\Tools\SchemaTool($this->em); - $schemaTool->dropSchema(array()); - $schemaTool->createSchema(array( - $this->em->getClassMetadata('Mapping\Fixture\Unmapped\Timestampable'), - )); + $schemaTool = new SchemaTool($this->em); + $schemaTool->dropSchema([]); + $schemaTool->createSchema([ + $this->em->getClassMetadata(Timestampable::class), + ]); } - /** - * @test - */ - public function shouldWork() + public function testShouldWork(): void { // driver falls back to annotation driver $conf = $this->timestampable->getConfiguration( $this->em, - 'Mapping\Fixture\Unmapped\Timestampable' + Timestampable::class ); - $this->assertTrue(isset($conf['create'])); + static::assertTrue(isset($conf['create'])); $test = new Timestampable(); $this->em->persist($test); $this->em->flush(); $id = $this->em - ->getClassMetadata('Mapping\Fixture\Unmapped\Timestampable') + ->getClassMetadata(Timestampable::class) ->getReflectionProperty('id') ->getValue($test) ; - $this->assertFalse(empty($id)); + static::assertNotEmpty($id); } } class CustomDriver implements MappingDriver { - public function getAllClassNames() + public function getAllClassNames(): array { - return array('Mapping\Fixture\Unmapped\Timestampable'); + return [Timestampable::class]; } - public function loadMetadataForClass($className, ClassMetadata $metadata) + public function loadMetadataForClass($className, ClassMetadata $metadata): void { - if ($className === 'Mapping\Fixture\Unmapped\Timestampable') { - $id = array(); + if (Timestampable::class === $className) { + $id = []; $id['fieldName'] = 'id'; $id['type'] = 'integer'; $id['nullable'] = false; @@ -86,7 +119,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) $metadata->mapField($id); - $created = array(); + $created = []; $created['fieldName'] = 'created'; $created['type'] = 'datetime'; $created['nullable'] = false; @@ -96,8 +129,8 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) } } - public function isTransient($className) + public function isTransient($className): bool { - return !in_array($className, $this->getAllClassNames()); + return !in_array($className, $this->getAllClassNames(), true); } } diff --git a/tests/Gedmo/Mapping/MetadataFactory/ForcedMetadataTest.php b/tests/Gedmo/Mapping/MetadataFactory/ForcedMetadataTest.php index 7d62d41674..80f19d7162 100644 --- a/tests/Gedmo/Mapping/MetadataFactory/ForcedMetadataTest.php +++ b/tests/Gedmo/Mapping/MetadataFactory/ForcedMetadataTest.php @@ -1,44 +1,110 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\MetadataFactory; + +use Doctrine\Common\EventManager; +use Doctrine\DBAL\DriverManager; +use Doctrine\ORM\Configuration; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Event\LoadClassMetadataEventArgs; +use Doctrine\ORM\Events; +use Doctrine\ORM\Id\IdentityGenerator; use Doctrine\ORM\Mapping\ClassMetadata; -use Mapping\Fixture\Unmapped\Timestampable; -use Doctrine\ORM\Version; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\Tools\SchemaTool; +use Gedmo\Mapping\Driver\AttributeReader; +use Gedmo\Tests\Mapping\Fixture\Unmapped\Timestampable; +use Gedmo\Timestampable\TimestampableListener; +use PHPUnit\Framework\TestCase; /** -* These are mapping tests for tree extension -* -* @author Gediminas Morkevicius -* @link http://www.gediminasm.org -* @license MIT License (http://www.opensource.org/licenses/mit-license.php) -*/ -class ForcedMetadataTest extends \PHPUnit_Framework_TestCase + * These are mapping tests for tree extension + * + * @author Gediminas Morkevicius + */ +final class ForcedMetadataTest extends TestCase { - public function setUp() + private TimestampableListener $timestampable; + + private EntityManager $em; + + protected function setUp(): void { - $config = new \Doctrine\ORM\Configuration(); - $config->setProxyDir(TESTS_TEMP_DIR); - $config->setProxyNamespace('Gedmo\Mapping\Proxy'); - $config->setMetadataDriverImpl( - new \Doctrine\ORM\Mapping\Driver\AnnotationDriver($_ENV['annotation_reader']) - ); + $config = new Configuration(); + + /** @phpstan-ignore-next-line function.alreadyNarrowedType */ + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'enableNativeLazyObjects')) { + $config->enableNativeLazyObjects(true); + } else { + $config->setProxyDir(TESTS_TEMP_DIR); + $config->setProxyNamespace('Gedmo\Mapping\Proxy'); + } + + if (PHP_VERSION_ID >= 80000) { + $config->setMetadataDriverImpl(new AttributeDriver([])); + } else { + $config->setMetadataDriverImpl(new AnnotationDriver($_ENV['annotation_reader'])); + } + + $this->timestampable = new TimestampableListener(); - $conn = array( + if (PHP_VERSION_ID >= 80000) { + $this->timestampable->setAnnotationReader(new AttributeReader()); + } else { + $this->timestampable->setAnnotationReader($_ENV['annotation_reader']); + } + + $evm = new EventManager(); + $evm->addEventSubscriber($this->timestampable); + + $connection = DriverManager::getConnection([ 'driver' => 'pdo_sqlite', 'memory' => true, + ], $config); + + $this->em = new EntityManager($connection, $config, $evm); + } + + public function testShouldWork(): void + { + $this->prepare(); + + // driver falls back to annotation driver + $conf = $this->timestampable->getConfiguration( + $this->em, + Timestampable::class ); - $evm = new \Doctrine\Common\EventManager(); - $this->timestampable = new \Gedmo\Timestampable\TimestampableListener(); - $this->timestampable->setAnnotationReader($_ENV['annotation_reader']); - $evm->addEventSubscriber($this->timestampable); - $this->em = \Doctrine\ORM\EntityManager::create($conn, $config, $evm); + // @todo: This assertion fails when run in isolation + static::assertTrue(isset($conf['create'])); + + $test = new Timestampable(); + $this->em->persist($test); + $this->em->flush(); + + $id = $this->em + ->getClassMetadata(Timestampable::class) + ->getReflectionProperty('id') + ->getValue($test) + ; + static::assertNotEmpty($id); } - private function prepare() + private function prepare(): void { $cmf = $this->em->getMetadataFactory(); - $metadata = new ClassMetadata('Mapping\Fixture\Unmapped\Timestampable'); - $id = array(); + $metadata = new ClassMetadata(Timestampable::class); + $id = []; $id['fieldName'] = 'id'; $id['type'] = 'integer'; $id['nullable'] = false; @@ -47,7 +113,7 @@ private function prepare() $metadata->mapField($id); - $created = array(); + $created = []; $created['fieldName'] = 'created'; $created['type'] = 'datetime'; $created['nullable'] = false; @@ -55,49 +121,20 @@ private function prepare() $metadata->mapField($created); $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY); - $metadata->setIdGenerator(new \Doctrine\ORM\Id\IdentityGenerator(null)); - $metadata->setPrimaryTable(array('name' => 'temp_test')); - $cmf->setMetadataFor('Mapping\Fixture\Unmapped\Timestampable', $metadata); + $metadata->setIdGenerator(new IdentityGenerator(null)); + $metadata->setPrimaryTable(['name' => 'temp_test']); + $cmf->setMetadataFor(Timestampable::class, $metadata); // trigger loadClassMetadata event $evm = $this->em->getEventManager(); - $eventArgs = new \Doctrine\ORM\Event\LoadClassMetadataEventArgs($metadata, $this->em); - $evm->dispatchEvent(\Doctrine\ORM\Events::loadClassMetadata, $eventArgs); - - if (Version::compare('2.3.0-dev') <= 0) { - $metadata->wakeupReflection($cmf->getReflectionService()); - } - $schemaTool = new \Doctrine\ORM\Tools\SchemaTool($this->em); - $schemaTool->dropSchema(array()); - $schemaTool->createSchema(array( - $this->em->getClassMetadata('Mapping\Fixture\Unmapped\Timestampable'), - )); - } - - /** - * @test - */ - public function shouldWork() - { - $this->prepare(); + $eventArgs = new LoadClassMetadataEventArgs($metadata, $this->em); + $evm->dispatchEvent(Events::loadClassMetadata, $eventArgs); - $meta = $this->em->getClassMetadata('Mapping\Fixture\Unmapped\Timestampable'); - // driver falls back to annotation driver - $conf = $this->timestampable->getConfiguration( - $this->em, - 'Mapping\Fixture\Unmapped\Timestampable' - ); - $this->assertTrue(isset($conf['create'])); - - $test = new Timestampable(); - $this->em->persist($test); - $this->em->flush(); - - $id = $this->em - ->getClassMetadata('Mapping\Fixture\Unmapped\Timestampable') - ->getReflectionProperty('id') - ->getValue($test) - ; - $this->assertFalse(empty($id)); + $metadata->wakeupReflection($cmf->getReflectionService()); + $schemaTool = new SchemaTool($this->em); + $schemaTool->dropSchema([]); + $schemaTool->createSchema([ + $this->em->getClassMetadata(Timestampable::class), + ]); } } diff --git a/tests/Gedmo/Mapping/Mock/EventSubscriberCustomMock.php b/tests/Gedmo/Mapping/Mock/EventSubscriberCustomMock.php index a988edb9f2..e8448a671e 100644 --- a/tests/Gedmo/Mapping/Mock/EventSubscriberCustomMock.php +++ b/tests/Gedmo/Mapping/Mock/EventSubscriberCustomMock.php @@ -1,23 +1,37 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Mock; + +use Doctrine\Common\EventArgs; +use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Mapping\MappedEventSubscriber; -class EventSubscriberCustomMock extends MappedEventSubscriber +/** + * @phpstan-extends MappedEventSubscriber + */ +final class EventSubscriberCustomMock extends MappedEventSubscriber { - protected function getNamespace() + public function getAdapter(EventArgs $args): AdapterInterface { - return __NAMESPACE__; + return $this->getEventAdapter($args); } - public function getAdapter($args) + public function getSubscribedEvents(): array { - return $this->getEventAdapter($args); + return []; } - public function getSubscribedEvents() + protected function getNamespace(): string { - return array(); + return __NAMESPACE__; } } diff --git a/tests/Gedmo/Mapping/Mock/EventSubscriberMock.php b/tests/Gedmo/Mapping/Mock/EventSubscriberMock.php index 35ed203637..94b88ce524 100644 --- a/tests/Gedmo/Mapping/Mock/EventSubscriberMock.php +++ b/tests/Gedmo/Mapping/Mock/EventSubscriberMock.php @@ -1,23 +1,37 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Mock; + +use Doctrine\Common\EventArgs; +use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Mapping\MappedEventSubscriber; -class EventSubscriberMock extends MappedEventSubscriber +/** + * @phpstan-extends MappedEventSubscriber + */ +final class EventSubscriberMock extends MappedEventSubscriber { - protected function getNamespace() + public function getAdapter(EventArgs $args): AdapterInterface { - return 'something'; + return $this->getEventAdapter($args); } - public function getAdapter($args) + public function getSubscribedEvents(): array { - return $this->getEventAdapter($args); + return []; } - public function getSubscribedEvents() + protected function getNamespace(): string { - return array(); + return 'something'; } } diff --git a/tests/Gedmo/Mapping/Mock/Extension/Encoder/EncoderListener.php b/tests/Gedmo/Mapping/Mock/Extension/Encoder/EncoderListener.php index 6a6bba7805..2ad5e3be44 100644 --- a/tests/Gedmo/Mapping/Mock/Extension/Encoder/EncoderListener.php +++ b/tests/Gedmo/Mapping/Mock/Extension/Encoder/EncoderListener.php @@ -1,24 +1,41 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Mock\Extension\Encoder; use Doctrine\Common\EventArgs; -use Gedmo\Mapping\MappedEventSubscriber; +use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Mapping\Event\AdapterInterface as EventAdapterInterface; +use Gedmo\Mapping\MappedEventSubscriber; +/** + * @phpstan-extends MappedEventSubscriber + */ class EncoderListener extends MappedEventSubscriber { - public function getSubscribedEvents() + public function getSubscribedEvents(): array { - return array( + return [ 'onFlush', 'loadClassMetadata', - ); + ]; } - public function loadClassMetadata(EventArgs $args) + /** + * @phpstan-param LoadClassMetadataEventArgs, ObjectManager> $args + */ + public function loadClassMetadata(LoadClassMetadataEventArgs $args): void { $ea = $this->getEventAdapter($args); // this will check for our metadata @@ -28,7 +45,7 @@ public function loadClassMetadata(EventArgs $args) ); } - public function onFlush(EventArgs $args) + public function onFlush(EventArgs $args): void { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); @@ -38,7 +55,7 @@ public function onFlush(EventArgs $args) foreach ($ea->getScheduledObjectUpdates($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); // if it has our metadata lets encode the properties - if ($config = $this->getConfiguration($om, $meta->name)) { + if ($config = $this->getConfiguration($om, $meta->getName())) { $this->encode($ea, $object, $config); } } @@ -46,19 +63,22 @@ public function onFlush(EventArgs $args) foreach ($ea->getScheduledObjectInsertions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); // if it has our metadata lets encode the properties - if ($config = $this->getConfiguration($om, $meta->name)) { + if ($config = $this->getConfiguration($om, $meta->getName())) { $this->encode($ea, $object, $config); } } } - protected function getNamespace() + protected function getNamespace(): string { // mapper must know the namespace of extension return __NAMESPACE__; } - private function encode(EventAdapterInterface $ea, $object, $config) + /** + * @param array $config + */ + private function encode(EventAdapterInterface $ea, object $object, array $config): void { $om = $ea->getObjectManager(); $meta = $om->getClassMetadata(get_class($object)); diff --git a/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Annotations.php b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Annotations.php deleted file mode 100644 index 116fe2af91..0000000000 --- a/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Annotations.php +++ /dev/null @@ -1,16 +0,0 @@ - http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ -use Gedmo\Mapping\Driver; -use Doctrine\Common\Annotations\AnnotationReader; +namespace Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping\Driver; -class Annotation implements Driver -{ - /** - * original driver if it is available - */ - protected $_originalDriver = null; - - public function readExtendedMetadata($meta, array &$config) - { - // load our available annotations - require_once __DIR__.'/../Annotations.php'; - $reader = new AnnotationReader(); - // set annotation namespace and alias - //$reader->setAnnotationNamespaceAlias('Gedmo\Mapping\Mock\Extension\Encoder\Mapping\\', 'ext'); +use Gedmo\Mapping\Driver\AnnotationDriverInterface; - $class = $meta->getReflectionClass(); - // check only property annotations - foreach ($class->getProperties() as $property) { - // skip inherited properties - if ($meta->isMappedSuperclass && !$property->isPrivate() || - $meta->isInheritedField($property->name) || - isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - // now lets check if property has our annotation - if ($encode = $reader->getPropertyAnnotation($property, 'Gedmo\Mapping\Mock\Extension\Encoder\Mapping\Encode')) { - $field = $property->getName(); - // check if field is mapped - if (!$meta->hasField($field)) { - throw new \Exception("Field is not mapped as object property"); - } - // allow encoding only strings - if (!in_array($encode->type, array('sha1', 'md5'))) { - throw new \Exception("Invalid encoding type supplied"); - } - // validate encoding type - $mapping = $meta->getFieldMapping($field); - if ($mapping['type'] != 'string') { - throw new \Exception("Only strings can be encoded"); - } - // store the metadata - $config['encode'][$field] = array( - 'type' => $encode->type, - 'secret' => $encode->secret, - ); - } - } - } - - /** - * Passes in the mapping read by original driver - * - * @param $driver - * @return void - */ - public function setOriginalDriver($driver) - { - $this->_originalDriver = $driver; - } +class Annotation extends Attribute implements AnnotationDriverInterface +{ } diff --git a/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Driver/Attribute.php b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Driver/Attribute.php new file mode 100644 index 0000000000..2794af8934 --- /dev/null +++ b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Driver/Attribute.php @@ -0,0 +1,66 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping\Driver; + +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping\Encode; + +class Attribute extends AbstractAnnotationDriver +{ + public function readExtendedMetadata($meta, array &$config) + { + $class = $meta->getReflectionClass(); + + // check only property annotations + foreach ($class->getProperties() as $property) { + // skip inherited properties + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + // now lets check if property has our annotation + if ($encode = $this->reader->getPropertyAnnotation($property, Encode::class)) { + \assert($encode instanceof Encode); + + $field = $property->getName(); + + // check if field is mapped + if (!$meta->hasField($field)) { + throw new \Exception('Field is not mapped as object property'); + } + + // allow encoding only strings + if (!in_array($encode->type, ['sha1', 'md5'], true)) { + throw new \Exception('Invalid encoding type supplied'); + } + + // validate encoding type + $mapping = $meta->getFieldMapping($field); + + if ('string' !== ($mapping->type ?? $mapping['type'])) { + throw new \Exception('Only strings can be encoded'); + } + + // store the metadata + $config['encode'][$field] = [ + 'type' => $encode->type, + 'secret' => $encode->secret, + ]; + } + } + + return $config; + } +} diff --git a/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Encode.php b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Encode.php new file mode 100644 index 0000000000..eb6c5ebc1e --- /dev/null +++ b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Encode.php @@ -0,0 +1,39 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping; + +use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; + +/** + * @Annotation + * + * @NamedArgumentConstructor + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class Encode implements GedmoAnnotation +{ + /** + * @var string + */ + public $type = 'md5'; + + /** + * @var string|null + */ + public $secret; + + public function __construct(string $type = 'md5', ?string $secret = null) + { + $this->type = $type; + $this->secret = $secret; + } +} diff --git a/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Event/Adapter/ODM.php b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Event/Adapter/ODM.php index 1138956c1c..2a068ccc54 100644 --- a/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Event/Adapter/ODM.php +++ b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Event/Adapter/ODM.php @@ -1,6 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping\Event\Adapter; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; diff --git a/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Event/Adapter/ORM.php b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Event/Adapter/ORM.php index 4305a505ce..2f0d345f3f 100644 --- a/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Event/Adapter/ORM.php +++ b/tests/Gedmo/Mapping/Mock/Extension/Encoder/Mapping/Event/Adapter/ORM.php @@ -1,6 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Mock\Extension\Encoder\Mapping\Event\Adapter; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; diff --git a/tests/Gedmo/Mapping/Mock/Mapping/Event/Adapter/ORM.php b/tests/Gedmo/Mapping/Mock/Mapping/Event/Adapter/ORM.php index 448c1d9e18..bb3bf5d1f1 100644 --- a/tests/Gedmo/Mapping/Mock/Mapping/Event/Adapter/ORM.php +++ b/tests/Gedmo/Mapping/Mock/Mapping/Event/Adapter/ORM.php @@ -1,6 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Mock\Mapping\Event\Adapter; use Gedmo\Mapping\Event\Adapter\ORM as EventAdapterORM; diff --git a/tests/Gedmo/Mapping/MultiManagerMappingTest.php b/tests/Gedmo/Mapping/MultiManagerMappingTest.php index 7a0a3c401a..a1f2175f35 100644 --- a/tests/Gedmo/Mapping/MultiManagerMappingTest.php +++ b/tests/Gedmo/Mapping/MultiManagerMappingTest.php @@ -1,101 +1,116 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\ORM\Mapping\Driver\DriverChain; -use Doctrine\ORM\Mapping\Driver\YamlDriver; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; -use Tool\BaseTestCaseOM; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\Mapping\Driver\XmlDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\Tests\Mapping\Fixture\Xml\User; +use Gedmo\Tests\Sluggable\Fixture\Article as ArticleEntity; +use Gedmo\Tests\Sluggable\Fixture\Document\Article as ArticleDocument; +use Gedmo\Tests\Tool\BaseTestCaseOM; +use Gedmo\Tests\Translatable\Fixture\PersonTranslation; /** * These are mapping extension tests * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MultiManagerMappingTest extends BaseTestCaseOM +final class MultiManagerMappingTest extends BaseTestCaseOM { - /** - * @var Doctrine\ORM\EntityManager - */ - private $em1; - - /** - * @var Doctrine\ORM\EntityManager - */ - private $em2; - - /** - * @var Doctrine\ODM\MongoDB\DocumentManager - */ - private $dm1; - - public function setUp() + private EntityManager $em1; + + private EntityManager $em2; + + private DocumentManager $dm1; + + protected function setUp(): void { parent::setUp(); - // EM with standard annotation mapping - $this->em1 = $this->getMockSqliteEntityManager(array( - 'Sluggable\Fixture\Article', - )); - // EM with yaml and annotation mapping - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); - - $reader = new AnnotationReader(); - $annotationDriver2 = new AnnotationDriver($reader); - - $yamlDriver = new YamlDriver(__DIR__.'/Driver/Yaml'); - - $chain = new DriverChain(); - $chain->addDriver($annotationDriver, 'Translatable\Fixture'); - $chain->addDriver($yamlDriver, 'Mapping\Fixture\Yaml'); + + // EM with standard annotation/attribute mapping + $this->em1 = $this->getDefaultMockSqliteEntityManager([ + ArticleEntity::class, + ]); + + // EM with XML and annotation/attribute mapping + if (PHP_VERSION_ID >= 80000) { + $annotationDriver = new AttributeDriver([]); + + $annotationDriver2 = new AttributeDriver([]); + } else { + $reader = new AnnotationReader(); + $annotationDriver = new AnnotationDriver($reader); + + $reader = new AnnotationReader(); + $annotationDriver2 = new AnnotationDriver($reader); + } + + $xmlDriver = new XmlDriver(__DIR__.'/Driver/Xml', XmlDriver::DEFAULT_FILE_EXTENSION, false); + + $chain = new MappingDriverChain(); + $chain->addDriver($annotationDriver, 'Gedmo\Tests\Translatable\Fixture'); + $chain->addDriver($xmlDriver, 'Gedmo\Tests\Mapping\Fixture\Xml'); $chain->addDriver($annotationDriver2, 'Gedmo\Translatable'); - $this->em2 = $this->getMockSqliteEntityManager(array( - 'Translatable\Fixture\PersonTranslation', - 'Mapping\Fixture\Yaml\User', - ), $chain); - // DM with standard annotation mapping + $this->em2 = $this->getDefaultMockSqliteEntityManager([ + PersonTranslation::class, + User::class, + ], $chain); + + // DM with standard annotation/attribute mapping $this->dm1 = $this->getMockDocumentManager('gedmo_extensions_test'); } - public function testTwoDiferentManager() + public function testTwoDifferentManagers(): void { - $meta = $this->dm1->getClassMetadata('Sluggable\Fixture\Document\Article'); - $dmArticle = new \Sluggable\Fixture\Document\Article(); + // Force metadata class loading. + $this->dm1->getClassMetadata(ArticleDocument::class); + $dmArticle = new ArticleDocument(); $dmArticle->setCode('code'); $dmArticle->setTitle('title'); $this->dm1->persist($dmArticle); $this->dm1->flush(); - $this->assertEquals('title-code', $dmArticle->getSlug()); - $em1Article = new \Sluggable\Fixture\Article(); + static::assertSame('title-code', $dmArticle->getSlug()); + $em1Article = new ArticleEntity(); $em1Article->setCode('code'); $em1Article->setTitle('title'); $this->em1->persist($em1Article); $this->em1->flush(); - $this->assertEquals('title-code', $em1Article->getSlug()); + static::assertSame('title-code', $em1Article->getSlug()); } - public function testTwoSameManagers() + public function testTwoSameManagers(): void { - $em1Article = new \Sluggable\Fixture\Article(); + $em1Article = new ArticleEntity(); $em1Article->setCode('code'); $em1Article->setTitle('title'); $this->em1->persist($em1Article); $this->em1->flush(); - $this->assertEquals('title-code', $em1Article->getSlug()); + static::assertSame('title-code', $em1Article->getSlug()); - $user = new \Mapping\Fixture\Yaml\User(); + $user = new User(); $user->setUsername('user'); $user->setPassword('secret'); $this->em2->persist($user); $this->em2->flush(); - $this->assertEquals(1, $user->getId()); + static::assertSame(1, $user->getId()); } } diff --git a/tests/Gedmo/Mapping/ORMMappingTestCase.php b/tests/Gedmo/Mapping/ORMMappingTestCase.php new file mode 100644 index 0000000000..f5270ce8e3 --- /dev/null +++ b/tests/Gedmo/Mapping/ORMMappingTestCase.php @@ -0,0 +1,91 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\EventManager; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\DriverManager; +use Doctrine\ORM\Configuration; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\Mapping\Driver\XmlDriver; +use Doctrine\ORM\Mapping\Driver\YamlDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use PHPUnit\Framework\TestCase; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +abstract class ORMMappingTestCase extends TestCase +{ + /** + * @var CacheItemPoolInterface + */ + protected $cache; + + protected function setUp(): void + { + $this->cache = new ArrayAdapter(); + } + + final protected function getBasicConfiguration(): Configuration + { + $config = new Configuration(); + $config->setMetadataCache(new ArrayAdapter()); + $config->setQueryCache(new ArrayAdapter()); + + /** @phpstan-ignore-next-line function.alreadyNarrowedType */ + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'enableNativeLazyObjects')) { + $config->enableNativeLazyObjects(true); + } else { + $config->setProxyDir(TESTS_TEMP_DIR); + $config->setProxyNamespace('Gedmo\Mapping\Proxy'); + } + + return $config; + } + + final protected function getBasicEntityManager(?Configuration $config = null, ?Connection $connection = null, ?EventManager $evm = null): EntityManager + { + if (null === $config) { + $config = $this->getBasicConfiguration(); + $config->setMetadataDriverImpl($this->createChainedMappingDriver()); + } + + $connection ??= DriverManager::getConnection([ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ], $config); + + return new EntityManager($connection, $config, $evm); + } + + final protected function createChainedMappingDriver(): MappingDriverChain + { + $chain = new MappingDriverChain(); + + $chain->addDriver(new XmlDriver(__DIR__.'/Driver/Xml', XmlDriver::DEFAULT_FILE_EXTENSION, false), 'Gedmo\Tests\Mapping\Fixture\Xml'); + + if (class_exists(YamlDriver::class)) { + $chain->addDriver(new YamlDriver(__DIR__.'/Driver/Yaml'), 'Gedmo\Tests\Mapping\Fixture\Yaml'); + } + + if (PHP_VERSION_ID >= 80000) { + $chain->addDriver(new AttributeDriver([]), 'Gedmo\Tests\Mapping\Fixture'); + } elseif (class_exists(AnnotationDriver::class) && class_exists(AnnotationReader::class)) { + $chain->addDriver(new AnnotationDriver(new AnnotationReader()), 'Gedmo\Tests\Mapping\Fixture'); + } + + return $chain; + } +} diff --git a/tests/Gedmo/Mapping/ReferenceIntegrityMappingTest.php b/tests/Gedmo/Mapping/ReferenceIntegrityMappingTest.php index cbbb55a26f..065c8955a0 100644 --- a/tests/Gedmo/Mapping/ReferenceIntegrityMappingTest.php +++ b/tests/Gedmo/Mapping/ReferenceIntegrityMappingTest.php @@ -1,38 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; use Doctrine\Common\EventManager; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\Driver\YamlDriver; use Gedmo\ReferenceIntegrity\ReferenceIntegrityListener; -use Tool\BaseTestCaseOM; +use Gedmo\Tests\Mapping\Fixture\Yaml\Referenced; +use Gedmo\Tests\Mapping\Fixture\Yaml\Referencer; +use Gedmo\Tests\Tool\BaseTestCaseOM; /** * These are mapping tests for ReferenceIntegrity extension * * @author Jonathan Eskew * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ReferenceIntegrityMappingTest extends BaseTestCaseOM +final class ReferenceIntegrityMappingTest extends BaseTestCaseOM { - /** - * @var DocumentManager - */ - private $dm; + private DocumentManager $dm; - /** - * @var ReferenceIntegrityListener - */ - private $referenceIntegrity; + private ReferenceIntegrityListener $referenceIntegrity; - public function setUp() + protected function setUp(): void { - if (!class_exists('Doctrine\ODM\MongoDB\Mapping\Driver\YamlDriver')) { - $this->markTestSkipped('The Mongo ODM is not installed'); - } + static::markTestSkipped('Intentionally skipping test. Doctrine MongoDB ODM 2.0 removed the YAML mapping driver; skipping test until it can be rewritten using a supported mapper.'); parent::setUp(); @@ -45,19 +46,19 @@ public function setUp() $this->dm = $this->getMockDocumentManager('gedmo_extensions_test', $yamlDriver); } - public function testYamlMapping() + public function testYamlMapping(): void { - $referencerMeta = $this->dm->getClassMetadata('Mapping\Fixture\Yaml\Referencer'); - $referenceeMeta = $this->dm->getClassMetadata('Mapping\Fixture\Yaml\Referenced'); - $config = $this->referenceIntegrity->getConfiguration($this->dm, $referencerMeta->name); + $referencerMeta = $this->dm->getClassMetadata(Referencer::class); + $referenceeMeta = $this->dm->getClassMetadata(Referenced::class); + $config = $this->referenceIntegrity->getConfiguration($this->dm, $referencerMeta->getName()); - $this->assertNotEmpty($config['referenceIntegrity']); + static::assertNotEmpty($config['referenceIntegrity']); foreach ($config['referenceIntegrity'] as $propertyName => $referenceConfiguration) { - $this->assertArrayHasKey($propertyName, $referencerMeta->reflFields); + static::assertArrayHasKey($propertyName, $referencerMeta->reflFields); foreach ($referenceConfiguration as $inversedPropertyName => $integrityType) { - $this->assertArrayHasKey($inversedPropertyName, $referenceeMeta->reflFields); - $this->assertTrue(in_array($integrityType, ['nullify', 'restrict'])); + static::assertArrayHasKey($inversedPropertyName, $referenceeMeta->reflFields); + static::assertContains($integrityType, ['nullify', 'restrict']); } } } diff --git a/tests/Gedmo/Mapping/SluggableMappingTest.php b/tests/Gedmo/Mapping/SluggableMappingTest.php index 657ef248a7..9b9ad8cce6 100644 --- a/tests/Gedmo/Mapping/SluggableMappingTest.php +++ b/tests/Gedmo/Mapping/SluggableMappingTest.php @@ -1,141 +1,163 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\Mapping\Driver\YamlDriver; -use Doctrine\ORM\Mapping\Driver\DriverChain; -use Mapping\Fixture\Yaml\Category; use Gedmo\Mapping\ExtensionMetadataFactory; +use Gedmo\Sluggable\Handler\RelativeSlugHandler; +use Gedmo\Sluggable\Handler\TreeSlugHandler; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Mapping\Fixture\Sluggable as AnnotatedSluggable; +use Gedmo\Tests\Mapping\Fixture\Xml\Sluggable as XmlSluggable; +use Gedmo\Tests\Mapping\Fixture\Yaml\Category; /** * These are mapping tests for sluggable extension * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SluggableMappingTest extends \PHPUnit_Framework_TestCase +final class SluggableMappingTest extends ORMMappingTestCase { - const TEST_YAML_ENTITY_CLASS = 'Mapping\Fixture\Yaml\Category'; - const SLUGGABLE = 'Mapping\Fixture\Sluggable'; - private $em; + private EntityManager $em; - public function setUp() + protected function setUp(): void { - $config = new \Doctrine\ORM\Configuration(); - $config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); - $config->setQueryCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); - $config->setProxyDir(TESTS_TEMP_DIR); - $config->setProxyNamespace('Gedmo\Mapping\Proxy'); - $chainDriverImpl = new DriverChain(); - $chainDriverImpl->addDriver( - new YamlDriver(array(__DIR__.'/Driver/Yaml')), - 'Mapping\Fixture\Yaml' - ); - $reader = new \Doctrine\Common\Annotations\AnnotationReader(); - \Doctrine\Common\Annotations\AnnotationRegistry::registerAutoloadNamespace( - 'Gedmo\\Mapping\\Annotation', - VENDOR_PATH.'/../lib' - ); - $chainDriverImpl->addDriver( - new \Doctrine\ORM\Mapping\Driver\AnnotationDriver($reader), - 'Mapping\Fixture' - ); - $config->setMetadataDriverImpl($chainDriverImpl); - - $conn = array( - 'driver' => 'pdo_sqlite', - 'memory' => true, - ); - - $evm = new \Doctrine\Common\EventManager(); - $evm->addEventSubscriber(new SluggableListener()); - $this->em = \Doctrine\ORM\EntityManager::create($conn, $config, $evm); + parent::setUp(); + + $listener = new SluggableListener(); + $listener->setCacheItemPool($this->cache); + + $this->em = $this->getBasicEntityManager(); + $this->em->getEventManager()->addEventSubscriber($listener); } /** - * @test + * @return \Generator */ - public function shouldBeAbleToMapSluggableUsingYamlDriver() + public static function dataSluggableObject(): \Generator { - $meta = $this->em->getClassMetadata(self::TEST_YAML_ENTITY_CLASS); - $cacheId = ExtensionMetadataFactory::getCacheId( - self::TEST_YAML_ENTITY_CLASS, - 'Gedmo\Sluggable' - ); - $config = $this->em->getMetadataFactory()->getCacheDriver()->fetch($cacheId); - - $this->assertArrayHasKey('slugs', $config); - $this->assertArrayHasKey('slug', $config['slugs']); - $this->assertEquals('slug', $config['slugs']['slug']['slug']); - $this->assertArrayHasKey('fields', $config['slugs']['slug']); - $this->assertCount(1, $config['slugs']['slug']['fields']); - $this->assertEquals('title', $config['slugs']['slug']['fields'][0]); - - $this->assertArrayHasKey('style', $config['slugs']['slug']); - $this->assertEquals('camel', $config['slugs']['slug']['style']); - $this->assertArrayHasKey('separator', $config['slugs']['slug']); - $this->assertEquals('_', $config['slugs']['slug']['separator']); - $this->assertArrayHasKey('unique', $config['slugs']['slug']); - $this->assertTrue($config['slugs']['slug']['unique']); - $this->assertArrayHasKey('updatable', $config['slugs']['slug']); - $this->assertTrue($config['slugs']['slug']['updatable']); - - $this->assertArrayHasKey('handlers', $config['slugs']['slug']); - $handlers = $config['slugs']['slug']['handlers']; - $this->assertEquals(2, count($handlers)); - $this->assertArrayHasKey('Gedmo\Sluggable\Handler\TreeSlugHandler', $handlers); - $this->assertArrayHasKey('Gedmo\Sluggable\Handler\RelativeSlugHandler', $handlers); - - $first = $handlers['Gedmo\Sluggable\Handler\TreeSlugHandler']; - $this->assertEquals(2, count($first)); - $this->assertArrayHasKey('parentRelationField', $first); - $this->assertArrayHasKey('separator', $first); - $this->assertEquals('parent', $first['parentRelationField']); - $this->assertEquals('/', $first['separator']); - - $second = $handlers['Gedmo\Sluggable\Handler\RelativeSlugHandler']; - $this->assertEquals(3, count($second)); - $this->assertArrayHasKey('relationField', $second); - $this->assertArrayHasKey('relationSlugField', $second); - $this->assertArrayHasKey('separator', $second); - $this->assertEquals('parent', $second['relationField']); - $this->assertEquals('slug', $second['relationSlugField']); - $this->assertEquals('/', $second['separator']); + yield 'Model with XML mapping' => [XmlSluggable::class]; + + if (PHP_VERSION_ID >= 80000) { + yield 'Model with attributes' => [AnnotatedSluggable::class]; + } elseif (class_exists(AnnotationDriver::class)) { + yield 'Model with annotations' => [AnnotatedSluggable::class]; + } } /** - * @test + * @param class-string $className + * + * @dataProvider dataSluggableObject */ - public function shouldBeAbleToMapSluggableUsingAnnotationDriver() + public function testSluggableMapping(string $className): void + { + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Sluggable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('slugs', $config); + static::assertArrayHasKey('slug', $config['slugs']); + static::assertSame('slug', $config['slugs']['slug']['slug']); + static::assertArrayHasKey('fields', $config['slugs']['slug']); + static::assertCount(3, $config['slugs']['slug']['fields']); + static::assertSame('title', $config['slugs']['slug']['fields'][0]); + static::assertSame('ean', $config['slugs']['slug']['fields'][1]); + static::assertSame('code', $config['slugs']['slug']['fields'][2]); + + static::assertArrayHasKey('style', $config['slugs']['slug']); + static::assertSame('camel', $config['slugs']['slug']['style']); + static::assertArrayHasKey('separator', $config['slugs']['slug']); + static::assertSame('_', $config['slugs']['slug']['separator']); + static::assertArrayHasKey('unique', $config['slugs']['slug']); + static::assertTrue($config['slugs']['slug']['unique']); + static::assertArrayHasKey('updatable', $config['slugs']['slug']); + static::assertFalse($config['slugs']['slug']['updatable']); + + static::assertArrayHasKey('handlers', $config['slugs']['slug']); + $handlers = $config['slugs']['slug']['handlers']; + static::assertCount(2, $handlers); + static::assertArrayHasKey(TreeSlugHandler::class, $handlers); + static::assertArrayHasKey(RelativeSlugHandler::class, $handlers); + + $first = $handlers[TreeSlugHandler::class]; + static::assertCount(2, $first); + static::assertArrayHasKey('parentRelationField', $first); + static::assertArrayHasKey('separator', $first); + static::assertSame('parent', $first['parentRelationField']); + static::assertSame('/', $first['separator']); + + $second = $handlers[RelativeSlugHandler::class]; + static::assertCount(3, $second); + static::assertArrayHasKey('relationField', $second); + static::assertArrayHasKey('relationSlugField', $second); + static::assertArrayHasKey('separator', $second); + static::assertSame('parent', $second['relationField']); + static::assertSame('test', $second['relationSlugField']); + static::assertSame('-', $second['separator']); + } + + public function testSluggableYamlMapping(): void { - $meta = $this->em->getClassMetadata(self::SLUGGABLE); - $cacheId = ExtensionMetadataFactory::getCacheId( - self::SLUGGABLE, - 'Gedmo\Sluggable' - ); - $config = $this->em->getMetadataFactory()->getCacheDriver()->fetch($cacheId); - - $this->assertArrayHasKey('handlers', $config['slugs']['slug']); + if (!class_exists(YamlDriver::class)) { + static::markTestSkipped('Test case requires the deprecated YAML mapping driver from the ORM.'); + } + + $className = Category::class; + + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Sluggable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('slugs', $config); + static::assertArrayHasKey('slug', $config['slugs']); + static::assertSame('slug', $config['slugs']['slug']['slug']); + static::assertArrayHasKey('fields', $config['slugs']['slug']); + static::assertCount(1, $config['slugs']['slug']['fields']); + static::assertSame('title', $config['slugs']['slug']['fields'][0]); + + static::assertArrayHasKey('style', $config['slugs']['slug']); + static::assertSame('camel', $config['slugs']['slug']['style']); + static::assertArrayHasKey('separator', $config['slugs']['slug']); + static::assertSame('_', $config['slugs']['slug']['separator']); + static::assertArrayHasKey('unique', $config['slugs']['slug']); + static::assertTrue($config['slugs']['slug']['unique']); + static::assertArrayHasKey('updatable', $config['slugs']['slug']); + static::assertTrue($config['slugs']['slug']['updatable']); + + static::assertArrayHasKey('handlers', $config['slugs']['slug']); $handlers = $config['slugs']['slug']['handlers']; - $this->assertEquals(2, count($handlers)); - $this->assertArrayHasKey('Gedmo\Sluggable\Handler\TreeSlugHandler', $handlers); - $this->assertArrayHasKey('Gedmo\Sluggable\Handler\RelativeSlugHandler', $handlers); - - $first = $handlers['Gedmo\Sluggable\Handler\TreeSlugHandler']; - $this->assertEquals(2, count($first)); - $this->assertArrayHasKey('parentRelationField', $first); - $this->assertArrayHasKey('separator', $first); - $this->assertEquals('parent', $first['parentRelationField']); - $this->assertEquals('/', $first['separator']); - - $second = $handlers['Gedmo\Sluggable\Handler\RelativeSlugHandler']; - $this->assertEquals(3, count($second)); - $this->assertArrayHasKey('relationField', $second); - $this->assertArrayHasKey('relationSlugField', $second); - $this->assertArrayHasKey('separator', $second); - $this->assertEquals('user', $second['relationField']); - $this->assertEquals('slug', $second['relationSlugField']); - $this->assertEquals('/', $second['separator']); + static::assertCount(2, $handlers); + static::assertArrayHasKey(TreeSlugHandler::class, $handlers); + static::assertArrayHasKey(RelativeSlugHandler::class, $handlers); + + $first = $handlers[TreeSlugHandler::class]; + static::assertCount(2, $first); + static::assertArrayHasKey('parentRelationField', $first); + static::assertArrayHasKey('separator', $first); + static::assertSame('parent', $first['parentRelationField']); + static::assertSame('/', $first['separator']); + + $second = $handlers[RelativeSlugHandler::class]; + static::assertCount(3, $second); + static::assertArrayHasKey('relationField', $second); + static::assertArrayHasKey('relationSlugField', $second); + static::assertArrayHasKey('separator', $second); + static::assertSame('parent', $second['relationField']); + static::assertSame('slug', $second['relationSlugField']); + static::assertSame('/', $second['separator']); } } diff --git a/tests/Gedmo/Mapping/SoftDeleteableMappingTest.php b/tests/Gedmo/Mapping/SoftDeleteableMappingTest.php index 23a62cdfaf..0b868de5c2 100644 --- a/tests/Gedmo/Mapping/SoftDeleteableMappingTest.php +++ b/tests/Gedmo/Mapping/SoftDeleteableMappingTest.php @@ -1,68 +1,81 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; -use Doctrine\Common\EventManager; -use Doctrine\ORM\Mapping\Driver\DriverChain; use Doctrine\ORM\Mapping\Driver\YamlDriver; +use Gedmo\Mapping\ExtensionMetadataFactory; use Gedmo\SoftDeleteable\SoftDeleteableListener; -use Tool\BaseTestCaseOM; +use Gedmo\Tests\Mapping\Fixture\SoftDeleteable as AnnotatedSoftDeleteable; +use Gedmo\Tests\Mapping\Fixture\Xml\SoftDeleteable as XmlSoftDeleteable; +use Gedmo\Tests\Mapping\Fixture\Yaml\SoftDeleteable as YamlSoftDeleteable; /** * These are mapping tests for SoftDeleteable extension * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SoftDeleteableMappingTest extends BaseTestCaseOM +final class SoftDeleteableMappingTest extends ORMMappingTestCase { - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; - - /** - * @var Gedmo\SoftDeleteable\SoftDeleteableListener - */ - private $softDeleteable; + private EntityManager $em; - public function setUp() + protected function setUp(): void { parent::setUp(); - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); + $listener = new SoftDeleteableListener(); + $listener->setCacheItemPool($this->cache); - $yamlDriver = new YamlDriver(__DIR__.'/Driver/Yaml'); + $this->em = $this->getBasicEntityManager(); + $this->em->getEventManager()->addEventSubscriber($listener); + } - $chain = new DriverChain(); - $chain->addDriver($yamlDriver, 'Mapping\Fixture\Yaml'); - $chain->addDriver($annotationDriver, 'Mapping\Fixture'); + /** + * @return \Generator + */ + public static function dataSoftDeleteableObject(): \Generator + { + yield 'Model with XML mapping' => [XmlSoftDeleteable::class]; - $this->softDeleteable = new SoftDeleteableListener(); - $this->evm = new EventManager(); - $this->evm->addEventSubscriber($this->softDeleteable); + if (PHP_VERSION_ID >= 80000) { + yield 'Model with attributes' => [AnnotatedSoftDeleteable::class]; + } elseif (class_exists(AnnotationDriver::class)) { + yield 'Model with annotations' => [AnnotatedSoftDeleteable::class]; + } - $this->em = $this->getMockSqliteEntityManager(array( - 'Mapping\Fixture\Yaml\SoftDeleteable', - 'Mapping\Fixture\SoftDeleteable', - ), $chain); + if (class_exists(YamlDriver::class)) { + yield 'Model with YAML mapping' => [YamlSoftDeleteable::class]; + } } - public function testYamlMapping() + /** + * @param class-string $className + * + * @dataProvider dataSoftDeleteableObject + */ + public function testSoftDeleteableMapping(string $className): void { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Yaml\SoftDeleteable'); - $config = $this->softDeleteable->getConfiguration($this->em, $meta->name); + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\SoftDeleteable'); + $config = $this->cache->getItem($cacheId)->get(); - $this->assertArrayHasKey('softDeleteable', $config); - $this->assertTrue($config['softDeleteable']); - $this->assertArrayHasKey('timeAware', $config); - $this->assertFalse($config['timeAware']); - $this->assertArrayHasKey('fieldName', $config); - $this->assertEquals('deletedAt', $config['fieldName']); + static::assertArrayHasKey('softDeleteable', $config); + static::assertTrue($config['softDeleteable']); + static::assertArrayHasKey('timeAware', $config); + static::assertFalse($config['timeAware']); + static::assertArrayHasKey('fieldName', $config); + static::assertSame('deletedAt', $config['fieldName']); } } diff --git a/tests/Gedmo/Mapping/SortableMappingTest.php b/tests/Gedmo/Mapping/SortableMappingTest.php index 4fbc149db0..f95c8150f9 100644 --- a/tests/Gedmo/Mapping/SortableMappingTest.php +++ b/tests/Gedmo/Mapping/SortableMappingTest.php @@ -1,68 +1,81 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; -use Doctrine\Common\EventManager; -use Doctrine\ORM\Mapping\Driver\DriverChain; use Doctrine\ORM\Mapping\Driver\YamlDriver; +use Gedmo\Mapping\ExtensionMetadataFactory; use Gedmo\Sortable\SortableListener; -use Tool\BaseTestCaseOM; +use Gedmo\Tests\Mapping\Fixture\Sortable as AnnotatedSortable; +use Gedmo\Tests\Mapping\Fixture\Xml\Sortable as XmlSortable; +use Gedmo\Tests\Mapping\Fixture\Yaml\Sortable as YamlSortable; /** * These are mapping tests for sortable extension * * @author Lukas Botsch - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SortableMappingTest extends BaseTestCaseOM +final class SortableMappingTest extends ORMMappingTestCase { - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; - - /** - * @var Gedmo\Sortable\SortableListener - */ - private $sortable; + private EntityManager $em; - public function setUp() + protected function setUp(): void { parent::setUp(); - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); + $listener = new SortableListener(); + $listener->setCacheItemPool($this->cache); - $yamlDriver = new YamlDriver(__DIR__.'/Driver/Yaml'); + $this->em = $this->getBasicEntityManager(); + $this->em->getEventManager()->addEventSubscriber($listener); + } - $chain = new DriverChain(); - $chain->addDriver($yamlDriver, 'Mapping\Fixture\Yaml'); - $chain->addDriver($annotationDriver, 'Mapping\Fixture'); + /** + * @return \Generator + */ + public static function dataSortableObject(): \Generator + { + yield 'Model with XML mapping' => [XmlSortable::class]; - $this->sortable = new SortableListener(); - $this->evm = new EventManager(); - $this->evm->addEventSubscriber($this->sortable); + if (PHP_VERSION_ID >= 80000) { + yield 'Model with attributes' => [AnnotatedSortable::class]; + } elseif (class_exists(AnnotationDriver::class)) { + yield 'Model with annotations' => [AnnotatedSortable::class]; + } - $this->em = $this->getMockSqliteEntityManager(array( - 'Mapping\Fixture\Yaml\Sortable', - 'Mapping\Fixture\SortableGroup', - ), $chain); + if (class_exists(YamlDriver::class)) { + yield 'Model with YAML mapping' => [YamlSortable::class]; + } } - public function testYamlMapping() + /** + * @param class-string $className + * + * @dataProvider dataSortableObject + */ + public function testSortableMapping(string $className): void { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Yaml\Sortable'); - $config = $this->sortable->getConfiguration($this->em, $meta->name); + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Sortable'); + $config = $this->cache->getItem($cacheId)->get(); - $this->assertArrayHasKey('position', $config); - $this->assertEquals('position', $config['position']); - $this->assertArrayHasKey('groups', $config); - $this->assertCount(3, $config['groups']); - $this->assertEquals('grouping', $config['groups'][0]); - $this->assertEquals('sortable_group', $config['groups'][1]); - $this->assertEquals('sortable_groups', $config['groups'][2]); + static::assertArrayHasKey('position', $config); + static::assertSame('position', $config['position']); + static::assertArrayHasKey('groups', $config); + static::assertCount(3, $config['groups']); + static::assertSame('grouping', $config['groups'][0]); + static::assertSame('sortable_group', $config['groups'][1]); + static::assertSame('sortable_groups', $config['groups'][2]); } } diff --git a/tests/Gedmo/Mapping/TimestampableMappingTest.php b/tests/Gedmo/Mapping/TimestampableMappingTest.php index 261264f29f..91c3ec1d89 100644 --- a/tests/Gedmo/Mapping/TimestampableMappingTest.php +++ b/tests/Gedmo/Mapping/TimestampableMappingTest.php @@ -1,67 +1,105 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\Mapping\Driver\YamlDriver; -use Doctrine\ORM\Mapping\Driver\DriverChain; -use Mapping\Fixture\Yaml\Category; use Gedmo\Mapping\ExtensionMetadataFactory; +use Gedmo\Tests\Mapping\Fixture\Category as AnnotatedCategory; +use Gedmo\Tests\Mapping\Fixture\Xml\Timestampable; +use Gedmo\Tests\Mapping\Fixture\Yaml\Category as YamlCategory; +use Gedmo\Timestampable\TimestampableListener; /** * These are mapping tests for timestampable extension * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TimestampableMappingTest extends \PHPUnit_Framework_TestCase +final class TimestampableMappingTest extends ORMMappingTestCase { - const TEST_YAML_ENTITY_CLASS = 'Mapping\Fixture\Yaml\Category'; - private $em; + private EntityManager $em; - public function setUp() + protected function setUp(): void { - $config = new \Doctrine\ORM\Configuration(); - $config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); - $config->setQueryCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); - $config->setProxyDir(TESTS_TEMP_DIR); - $config->setProxyNamespace('Gedmo\Mapping\Proxy'); - $chainDriverImpl = new DriverChain(); - $chainDriverImpl->addDriver( - new YamlDriver(array(__DIR__.'/Driver/Yaml')), - 'Mapping\Fixture\Yaml' - ); - $config->setMetadataDriverImpl($chainDriverImpl); - - $conn = array( - 'driver' => 'pdo_sqlite', - 'memory' => true, - ); - - //$config->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger()); - - $evm = new \Doctrine\Common\EventManager(); - $evm->addEventSubscriber(new TimestampableListener()); - $this->em = \Doctrine\ORM\EntityManager::create($conn, $config, $evm); + parent::setUp(); + + $listener = new TimestampableListener(); + $listener->setCacheItemPool($this->cache); + + $this->em = $this->getBasicEntityManager(); + $this->em->getEventManager()->addEventSubscriber($listener); + } + + /** + * @return \Generator + * + * @note the XML fixture has a different mapping from the other configs, so it is tested separately + */ + public static function dataTimestampableObject(): \Generator + { + if (PHP_VERSION_ID >= 80000) { + yield 'Model with attributes' => [AnnotatedCategory::class]; + } elseif (class_exists(AnnotationDriver::class)) { + yield 'Model with annotations' => [AnnotatedCategory::class]; + } + + if (class_exists(YamlDriver::class)) { + yield 'Model with YAML mapping' => [YamlCategory::class]; + } } - public function testYamlMapping() + /** + * @param class-string $className + * + * @dataProvider dataTimestampableObject + */ + public function testTimestampableMapping(string $className): void { - $meta = $this->em->getClassMetadata(self::TEST_YAML_ENTITY_CLASS); - $cacheId = ExtensionMetadataFactory::getCacheId( - self::TEST_YAML_ENTITY_CLASS, - 'Gedmo\Timestampable' - ); - $config = $this->em->getMetadataFactory()->getCacheDriver()->fetch($cacheId); - $this->assertArrayHasKey('create', $config); - $this->assertEquals('created', $config['create'][0]); - $this->assertArrayHasKey('update', $config); - $this->assertEquals('updated', $config['update'][0]); - $this->assertArrayHasKey('change', $config); + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Timestampable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('create', $config); + static::assertSame('created', $config['create'][0]); + static::assertArrayHasKey('update', $config); + static::assertSame('updated', $config['update'][0]); + static::assertArrayHasKey('change', $config); + $onChange = $config['change'][0]; + + static::assertSame('changed', $onChange['field']); + static::assertSame('title', $onChange['trackedField']); + static::assertSame('Test', $onChange['value']); + } + + public function testTimestampableXmlMapping(): void + { + $className = Timestampable::class; + + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Timestampable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('create', $config); + static::assertSame('created', $config['create'][0]); + static::assertArrayHasKey('update', $config); + static::assertSame('updated', $config['update'][0]); + static::assertArrayHasKey('change', $config); $onChange = $config['change'][0]; - $this->assertEquals('changed', $onChange['field']); - $this->assertEquals('title', $onChange['trackedField']); - $this->assertEquals('Test', $onChange['value']); + static::assertSame('published', $onChange['field']); + static::assertSame('status.title', $onChange['trackedField']); + static::assertSame('Published', $onChange['value']); } } diff --git a/tests/Gedmo/Mapping/TranslatableMappingTest.php b/tests/Gedmo/Mapping/TranslatableMappingTest.php index 04eda430a0..4edec0a40e 100644 --- a/tests/Gedmo/Mapping/TranslatableMappingTest.php +++ b/tests/Gedmo/Mapping/TranslatableMappingTest.php @@ -1,69 +1,86 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\Mapping\Driver\YamlDriver; -use Doctrine\ORM\Mapping\Driver\DriverChain; -use Mapping\Fixture\Yaml\User; use Gedmo\Mapping\ExtensionMetadataFactory; +use Gedmo\Tests\Mapping\Fixture\User as AnnotatedUser; +use Gedmo\Tests\Mapping\Fixture\Xml\User as XmlUser; +use Gedmo\Tests\Mapping\Fixture\Yaml\User as YamlUser; +use Gedmo\Tests\Translatable\Fixture\PersonTranslation; +use Gedmo\Translatable\TranslatableListener; /** * These are mapping tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableMappingTest extends \PHPUnit_Framework_TestCase +final class TranslatableMappingTest extends ORMMappingTestCase { - const TEST_YAML_ENTITY_CLASS = 'Mapping\Fixture\Yaml\User'; - private $em; + private EntityManager $em; - public function setUp() + protected function setUp(): void { - $config = new \Doctrine\ORM\Configuration(); - $config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); - $config->setQueryCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); - $config->setProxyDir(TESTS_TEMP_DIR); - $config->setProxyNamespace('Gedmo\Mapping\Proxy'); - $chainDriverImpl = new DriverChain(); - $chainDriverImpl->addDriver( - new YamlDriver(array(__DIR__.'/Driver/Yaml')), - 'Mapping\Fixture\Yaml' - ); - $config->setMetadataDriverImpl($chainDriverImpl); + parent::setUp(); + + $listener = new TranslatableListener(); + $listener->setCacheItemPool($this->cache); + $listener->setTranslatableLocale('en_us'); - $conn = array( - 'driver' => 'pdo_sqlite', - 'memory' => true, - ); + $this->em = $this->getBasicEntityManager(); + $this->em->getEventManager()->addEventSubscriber($listener); + } + + /** + * @return \Generator + */ + public static function dataSortableObject(): \Generator + { + yield 'Model with XML mapping' => [XmlUser::class]; - //$config->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger()); + if (PHP_VERSION_ID >= 80000) { + yield 'Model with attributes' => [AnnotatedUser::class]; + } elseif (class_exists(AnnotationDriver::class)) { + yield 'Model with annotations' => [AnnotatedUser::class]; + } - $evm = new \Doctrine\Common\EventManager(); - $this->translatableListener = new TranslatableListener(); - $this->translatableListener->setTranslatableLocale('en_us'); - $evm->addEventSubscriber($this->translatableListener); - $this->em = \Doctrine\ORM\EntityManager::create($conn, $config, $evm); + if (class_exists(YamlDriver::class)) { + yield 'Model with YAML mapping' => [YamlUser::class]; + } } - public function testYamlMapping() + /** + * @param class-string $className + * + * @dataProvider dataSortableObject + */ + public function testTranslatableMapping(string $className): void { - $meta = $this->em->getClassMetadata(self::TEST_YAML_ENTITY_CLASS); - $cacheId = ExtensionMetadataFactory::getCacheId( - self::TEST_YAML_ENTITY_CLASS, - 'Gedmo\Translatable' - ); - $config = $this->em->getMetadataFactory()->getCacheDriver()->fetch($cacheId); - $this->assertArrayHasKey('translationClass', $config); - $this->assertEquals('Translatable\Fixture\PersonTranslation', $config['translationClass']); - $this->assertArrayHasKey('fields', $config); - $this->assertCount(3, $config['fields']); - $this->assertEquals('password', $config['fields'][0]); - $this->assertEquals('username', $config['fields'][1]); - $this->assertArrayHasKey('locale', $config); - $this->assertEquals('localeField', $config['locale']); - $this->assertCount(1, $config['fallback']); - $this->assertTrue($config['fallback']['company']); + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Translatable'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('translationClass', $config); + static::assertSame(PersonTranslation::class, $config['translationClass']); + static::assertArrayHasKey('fields', $config); + static::assertCount(3, $config['fields']); + static::assertSame('password', $config['fields'][0]); + static::assertSame('username', $config['fields'][1]); + static::assertArrayHasKey('locale', $config); + static::assertSame('localeField', $config['locale']); + static::assertCount(1, $config['fallback']); + static::assertTrue($config['fallback']['company']); } } diff --git a/tests/Gedmo/Mapping/TreeMappingTest.php b/tests/Gedmo/Mapping/TreeMappingTest.php index cd8108165e..6d7a850f16 100644 --- a/tests/Gedmo/Mapping/TreeMappingTest.php +++ b/tests/Gedmo/Mapping/TreeMappingTest.php @@ -1,133 +1,153 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\ORM\Mapping\Driver\YamlDriver; -use Doctrine\ORM\Mapping\Driver\DriverChain; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use Gedmo\Mapping\ExtensionMetadataFactory; +use Gedmo\Tests\Mapping\Fixture\Yaml\Category; +use Gedmo\Tests\Mapping\Fixture\Yaml\ClosureCategory; +use Gedmo\Tests\Mapping\Fixture\Yaml\MaterializedPathCategory; +use Gedmo\Tests\Tree\Fixture\Closure\CategoryClosureWithoutMapping; +use Gedmo\Tree\TreeListener; /** * These are mapping tests for tree extension * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TreeMappingTest extends \PHPUnit_Framework_TestCase +final class TreeMappingTest extends ORMMappingTestCase { - const TEST_YAML_ENTITY_CLASS = 'Mapping\Fixture\Yaml\Category'; - const YAML_CLOSURE_CATEGORY = 'Mapping\Fixture\Yaml\ClosureCategory'; - const YAML_MATERIALIZED_PATH_CATEGORY = 'Mapping\Fixture\Yaml\MaterializedPathCategory'; + private EntityManager $em; - /** - * @var \Doctrine\ORM\EntityManager - */ - private $em; + private TreeListener $listener; - /** - * @var TreeListener - */ - private $listener; + public static function setUpBeforeClass(): void + { + if (!class_exists(YamlDriver::class)) { + static::markTestSkipped('Test requires deprecated ORM YAML mapping.'); + } + } - public function setUp() + protected function setUp(): void { - $config = new \Doctrine\ORM\Configuration(); - $config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); - $config->setQueryCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); - $config->setProxyDir(TESTS_TEMP_DIR); - $config->setProxyNamespace('Gedmo\Mapping\Proxy'); - $chainDriverImpl = new DriverChain(); - $chainDriverImpl->addDriver( - new YamlDriver(array(__DIR__.'/Driver/Yaml')), - 'Mapping\Fixture\Yaml' - ); - $chainDriverImpl->addDriver( - $config->newDefaultAnnotationDriver(array(), false), - 'Tree\Fixture' - ); - $chainDriverImpl->addDriver( - $config->newDefaultAnnotationDriver(array(), false), - 'Gedmo\Tree' - ); - $config->setMetadataDriverImpl($chainDriverImpl); + parent::setUp(); - $conn = array( - 'driver' => 'pdo_sqlite', - 'memory' => true, - ); + $config = $this->getBasicConfiguration(); + + $chain = new MappingDriverChain(); + + // TODO - The ORM's YAML mapping is deprecated and removed in 3.0 + $chain->addDriver(new YamlDriver(__DIR__.'/Driver/Yaml'), 'Gedmo\Tests\Mapping\Fixture\Yaml'); + + if (PHP_VERSION_ID >= 80000) { + $annotationOrAttributeDriver = new AttributeDriver([]); + } else { + $annotationOrAttributeDriver = new AnnotationDriver(new AnnotationReader()); + } + + $chain->addDriver($annotationOrAttributeDriver, 'Gedmo\Tests\Tree\Fixture'); + $chain->addDriver($annotationOrAttributeDriver, 'Gedmo\Tree'); + + $config->setMetadataDriverImpl($chain); $this->listener = new TreeListener(); - $evm = new \Doctrine\Common\EventManager(); - $evm->addEventSubscriber(new TreeListener()); - $this->em = \Doctrine\ORM\EntityManager::create($conn, $config, $evm); + $this->listener->setCacheItemPool($this->cache); + + $this->em = $this->getBasicEntityManager($config); + $this->em->getEventManager()->addEventSubscriber($this->listener); } - public function testApcCached() + /** + * @group legacy + * + * @see https://github.com/doctrine/persistence/pull/144 + * @see \Doctrine\Persistence\Mapping\AbstractClassMetadataFactory::getCacheKey() + */ + public function testApcCached(): void { - $this->em->getClassMetadata(self::YAML_CLOSURE_CATEGORY); - $this->em->getClassMetadata('Tree\Fixture\Closure\CategoryClosure'); + $this->em->getClassMetadata(ClosureCategory::class); + $this->em->getClassMetadata(CategoryClosureWithoutMapping::class); - $meta = $this->em->getMetadataFactory()->getCacheDriver()->fetch( - "Tree\\Fixture\\Closure\\CategoryClosure\$CLASSMETADATA" - ); - $this->assertTrue($meta->hasAssociation('ancestor')); - $this->assertTrue($meta->hasAssociation('descendant')); + $meta = $this->em->getConfiguration()->getMetadataCache()->getItem( + 'Gedmo__Tests__Tree__Fixture__Closure__CategoryClosureWithoutMapping__CLASSMETADATA__' + )->get(); + static::assertNotFalse($meta); + static::assertTrue($meta->hasAssociation('ancestor')); + static::assertTrue($meta->hasAssociation('descendant')); } - public function testYamlNestedMapping() + public function testYamlNestedMapping(): void { - $this->em->getClassMetadata(self::TEST_YAML_ENTITY_CLASS); + $this->em->getClassMetadata(Category::class); $cacheId = ExtensionMetadataFactory::getCacheId( - self::TEST_YAML_ENTITY_CLASS, + Category::class, 'Gedmo\Tree' ); - $config = $this->em->getMetadataFactory()->getCacheDriver()->fetch($cacheId); - $this->assertArrayHasKey('left', $config); - $this->assertEquals('left', $config['left']); - $this->assertArrayHasKey('right', $config); - $this->assertEquals('right', $config['right']); - $this->assertArrayHasKey('parent', $config); - $this->assertEquals('parent', $config['parent']); - $this->assertArrayHasKey('level', $config); - $this->assertEquals('level', $config['level']); - $this->assertArrayHasKey('root', $config); - $this->assertEquals('rooted', $config['root']); - $this->assertArrayHasKey('strategy', $config); - $this->assertEquals('nested', $config['strategy']); + $config = $this->cache->getItem($cacheId)->get(); + static::assertArrayHasKey('left', $config); + static::assertSame('left', $config['left']); + static::assertArrayHasKey('right', $config); + static::assertSame('right', $config['right']); + static::assertArrayHasKey('parent', $config); + static::assertSame('parent', $config['parent']); + static::assertArrayHasKey('level', $config); + static::assertSame('level', $config['level']); + static::assertArrayHasKey('root', $config); + static::assertSame('rooted', $config['root']); + static::assertArrayHasKey('strategy', $config); + static::assertSame('nested', $config['strategy']); } - public function testYamlClosureMapping() + /** + * @group legacy + */ + public function testYamlClosureMapping(): void { - $meta = $this->em->getClassMetadata(self::YAML_CLOSURE_CATEGORY); - $cacheId = ExtensionMetadataFactory::getCacheId(self::YAML_CLOSURE_CATEGORY, 'Gedmo\Tree'); - $config = $this->em->getMetadataFactory()->getCacheDriver()->fetch($cacheId); - - $this->assertArrayHasKey('parent', $config); - $this->assertEquals('parent', $config['parent']); - $this->assertArrayHasKey('strategy', $config); - $this->assertEquals('closure', $config['strategy']); - $this->assertArrayHasKey('closure', $config); - $this->assertEquals('Tree\\Fixture\\Closure\\CategoryClosure', $config['closure']); + // Force metadata class loading. + $this->em->getClassMetadata(ClosureCategory::class); + $cacheId = ExtensionMetadataFactory::getCacheId(ClosureCategory::class, 'Gedmo\Tree'); + $config = $this->cache->getItem($cacheId)->get(); + + static::assertArrayHasKey('parent', $config); + static::assertSame('parent', $config['parent']); + static::assertArrayHasKey('strategy', $config); + static::assertSame('closure', $config['strategy']); + static::assertArrayHasKey('closure', $config); + static::assertSame(CategoryClosureWithoutMapping::class, $config['closure']); } - public function testYamlMaterializedPathMapping() + public function testYamlMaterializedPathMapping(): void { - $meta = $this->em->getClassMetadata(self::YAML_MATERIALIZED_PATH_CATEGORY); - $config = $this->listener->getConfiguration($this->em, $meta->name); - - $this->assertArrayHasKey('strategy', $config); - $this->assertEquals('materializedPath', $config['strategy']); - $this->assertArrayHasKey('parent', $config); - $this->assertEquals('parent', $config['parent']); - $this->assertArrayHasKey('activate_locking', $config); - $this->assertTrue($config['activate_locking']); - $this->assertArrayHasKey('locking_timeout', $config); - $this->assertEquals(3, $config['locking_timeout']); - $this->assertArrayHasKey('level', $config); - $this->assertEquals('level', $config['level']); - $this->assertArrayHasKey('path', $config); - $this->assertEquals('path', $config['path']); - $this->assertArrayHasKey('path_separator', $config); - $this->assertEquals(',', $config['path_separator']); + $meta = $this->em->getClassMetadata(MaterializedPathCategory::class); + $config = $this->listener->getConfiguration($this->em, $meta->getName()); + + static::assertArrayHasKey('strategy', $config); + static::assertSame('materializedPath', $config['strategy']); + static::assertArrayHasKey('parent', $config); + static::assertSame('parent', $config['parent']); + static::assertArrayHasKey('activate_locking', $config); + static::assertTrue($config['activate_locking']); + static::assertArrayHasKey('locking_timeout', $config); + static::assertSame(3, $config['locking_timeout']); + static::assertArrayHasKey('level', $config); + static::assertSame('level', $config['level']); + static::assertArrayHasKey('path', $config); + static::assertSame('path', $config['path']); + static::assertArrayHasKey('path_separator', $config); + static::assertSame(',', $config['path_separator']); } } diff --git a/tests/Gedmo/Mapping/UploadableMappingTest.php b/tests/Gedmo/Mapping/UploadableMappingTest.php index f148c7c8dd..e76d13a6f0 100644 --- a/tests/Gedmo/Mapping/UploadableMappingTest.php +++ b/tests/Gedmo/Mapping/UploadableMappingTest.php @@ -1,79 +1,94 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping; + +use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; -use Doctrine\Common\EventManager; -use Doctrine\ORM\Mapping\Driver\DriverChain; use Doctrine\ORM\Mapping\Driver\YamlDriver; -use Gedmo\Uploadable\UploadableListener; +use Gedmo\Mapping\ExtensionMetadataFactory; +use Gedmo\Tests\Mapping\Fixture\Uploadable as AnnotatedUploadable; +use Gedmo\Tests\Mapping\Fixture\Xml\Uploadable as XmlUploadable; +use Gedmo\Tests\Mapping\Fixture\Yaml\Uploadable as YamlUploadable; use Gedmo\Uploadable\Mapping\Validator; -use Tool\BaseTestCaseOM; +use Gedmo\Uploadable\UploadableListener; /** * These are mapping tests for Uploadable extension * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class UploadableMappingTest extends BaseTestCaseOM +final class UploadableMappingTest extends ORMMappingTestCase { - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; - - /** - * @var Gedmo\SoftDeleteable\UploadableListener - */ - private $listener; + private EntityManager $em; - public function setUp() + protected function setUp(): void { parent::setUp(); + // TODO - This should be reset to default (true) after each test case Validator::$enableMimeTypesConfigException = false; - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); + $listener = new UploadableListener(); + $listener->setCacheItemPool($this->cache); - $yamlDriver = new YamlDriver(__DIR__.'/Driver/Yaml'); + $this->em = $this->getBasicEntityManager(); + $this->em->getEventManager()->addEventSubscriber($listener); + } - $chain = new DriverChain(); - $chain->addDriver($yamlDriver, 'Mapping\Fixture\Yaml'); - $chain->addDriver($annotationDriver, 'Mapping\Fixture'); + /** + * @return \Generator + */ + public static function dataUploadableObject(): \Generator + { + yield 'Model with XML mapping' => [XmlUploadable::class]; - $this->listener = new UploadableListener(); - $this->evm = new EventManager(); - $this->evm->addEventSubscriber($this->listener); + if (PHP_VERSION_ID >= 80000) { + yield 'Model with attributes' => [AnnotatedUploadable::class]; + } elseif (class_exists(AnnotationDriver::class)) { + yield 'Model with annotations' => [AnnotatedUploadable::class]; + } - $this->em = $this->getMockSqliteEntityManager(array( - 'Mapping\Fixture\Yaml\Uploadable', - ), $chain); + if (class_exists(YamlDriver::class)) { + yield 'Model with YAML mapping' => [YamlUploadable::class]; + } } - public function testYamlMapping() + /** + * @param class-string $className + * + * @dataProvider dataUploadableObject + */ + public function testUploadableMapping(string $className): void { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Yaml\Uploadable'); - $config = $this->listener->getConfiguration($this->em, $meta->name); + // Force metadata class loading. + $this->em->getClassMetadata($className); + $cacheId = ExtensionMetadataFactory::getCacheId($className, 'Gedmo\Uploadable'); + $config = $this->cache->getItem($cacheId)->get(); - $this->assertTrue($config['uploadable']); - $this->assertTrue($config['allowOverwrite']); - $this->assertTrue($config['appendNumber']); - $this->assertEquals('/my/path', $config['path']); - $this->assertEquals('getPath', $config['pathMethod']); - $this->assertEquals('mimeType', $config['fileMimeTypeField']); - $this->assertEquals('path', $config['filePathField']); - $this->assertEquals('size', $config['fileSizeField']); - $this->assertEquals('callbackMethod', $config['callback']); - $this->assertEquals('SHA1', $config['filenameGenerator']); - $this->assertEquals(1500, $config['maxSize']); - $this->assertContains('text/plain', $config['allowedTypes']); - $this->assertContains('text/css', $config['allowedTypes']); - $this->assertContains('video/jpeg', $config['disallowedTypes']); - $this->assertContains('text/html', $config['disallowedTypes']); + static::assertTrue($config['uploadable']); + static::assertTrue($config['allowOverwrite']); + static::assertTrue($config['appendNumber']); + static::assertSame('/my/path', $config['path']); + static::assertSame('getPath', $config['pathMethod']); + static::assertSame('mimeType', $config['fileMimeTypeField']); + static::assertSame('path', $config['filePathField']); + static::assertSame('size', $config['fileSizeField']); + static::assertSame('callbackMethod', $config['callback']); + static::assertSame(Validator::FILENAME_GENERATOR_SHA1, $config['filenameGenerator']); + static::assertSame(1500.0, $config['maxSize']); + static::assertContains('text/plain', $config['allowedTypes']); + static::assertContains('text/css', $config['allowedTypes']); + static::assertContains('video/jpeg', $config['disallowedTypes']); + static::assertContains('text/html', $config['disallowedTypes']); } } diff --git a/tests/Gedmo/Mapping/Xml/ClosureTreeMappingTest.php b/tests/Gedmo/Mapping/Xml/ClosureTreeMappingTest.php index 7add52a3e4..a8c140d826 100644 --- a/tests/Gedmo/Mapping/Xml/ClosureTreeMappingTest.php +++ b/tests/Gedmo/Mapping/Xml/ClosureTreeMappingTest.php @@ -1,68 +1,76 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Xml; use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\Common\EventManager; -use Doctrine\ORM\Mapping\Driver\DriverChain; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\ORM\Mapping\Driver\XmlDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\Tests\Mapping\Fixture\ClosureTreeClosure; +use Gedmo\Tests\Mapping\Fixture\Xml\ClosureTree; +use Gedmo\Tests\Tool\BaseTestCaseOM; use Gedmo\Tree\TreeListener; -use Tool\BaseTestCaseOM; /** * These are mapping extension tests * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ClosureTreeMappingTest extends BaseTestCaseOM +final class ClosureTreeMappingTest extends BaseTestCaseOM { - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; + private EntityManager $em; - /** - * @var Gedmo\Tree\TreeListener - */ - private $tree; + private TreeListener $tree; - public function setUp() + protected function setUp(): void { parent::setUp(); - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); + if (PHP_VERSION_ID >= 80000) { + $annotationDriver = new AttributeDriver([]); + } else { + $annotationDriver = new AnnotationDriver(new AnnotationReader()); + } - $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml'); + $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml', XmlDriver::DEFAULT_FILE_EXTENSION, false); - $chain = new DriverChain(); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); - $chain->addDriver($annotationDriver, 'Mapping\Fixture'); + $chain = new MappingDriverChain(); + $chain->addDriver($xmlDriver, 'Gedmo\Tests\Mapping\Fixture\Xml'); + $chain->addDriver($annotationDriver, 'Gedmo\Tests\Mapping\Fixture'); $chain->addDriver($annotationDriver, 'Gedmo\Tree'); $this->tree = new TreeListener(); $this->evm = new EventManager(); $this->evm->addEventSubscriber($this->tree); - $this->em = $this->getMockSqliteEntityManager(array( - 'Mapping\Fixture\Xml\ClosureTree', - 'Mapping\Fixture\ClosureTreeClosure', - ), $chain); + $this->em = $this->getDefaultMockSqliteEntityManager([ + ClosureTree::class, + ClosureTreeClosure::class, + ], $chain); } - public function testTreeMetadata() + public function testTreeMetadata(): void { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\ClosureTree'); - $config = $this->tree->getConfiguration($this->em, $meta->name); + $meta = $this->em->getClassMetadata(ClosureTree::class); + $config = $this->tree->getConfiguration($this->em, $meta->getName()); - $this->assertArrayHasKey('strategy', $config); - $this->assertEquals('closure', $config['strategy']); - $this->assertArrayHasKey('closure', $config); - $this->assertEquals('Mapping\Fixture\ClosureTreeClosure', $config['closure']); - $this->assertArrayHasKey('parent', $config); - $this->assertEquals('parent', $config['parent']); + static::assertArrayHasKey('strategy', $config); + static::assertSame('closure', $config['strategy']); + static::assertArrayHasKey('closure', $config); + static::assertSame(ClosureTreeClosure::class, $config['closure']); + static::assertArrayHasKey('parent', $config); + static::assertSame('parent', $config['parent']); } } diff --git a/tests/Gedmo/Mapping/Xml/LoggableMappingTest.php b/tests/Gedmo/Mapping/Xml/LoggableMappingTest.php deleted file mode 100644 index b4563e9769..0000000000 --- a/tests/Gedmo/Mapping/Xml/LoggableMappingTest.php +++ /dev/null @@ -1,90 +0,0 @@ - - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class LoggableMappingTest extends BaseTestCaseOM -{ - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; - - /** - * @var Gedmo\Loggable\LoggableListener - */ - private $loggable; - - public function setUp() - { - parent::setUp(); - - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); - - $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml'); - - $chain = new DriverChain(); - $chain->addDriver($annotationDriver, 'Gedmo\Loggable'); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); - - $this->loggable = new LoggableListener(); - $this->evm = new EventManager(); - $this->evm->addEventSubscriber($this->loggable); - - $this->em = $this->getMockSqliteEntityManager(array( - 'Gedmo\Loggable\Entity\LogEntry', - 'Mapping\Fixture\Xml\Loggable', - 'Mapping\Fixture\Xml\LoggableWithEmbedded', - 'Mapping\Fixture\Xml\Embedded', - 'Mapping\Fixture\Xml\Status', - ), $chain); - } - - public function testLoggableMetadata() - { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\Loggable'); - $config = $this->loggable->getConfiguration($this->em, $meta->name); - - $this->assertArrayHasKey('logEntryClass', $config); - $this->assertEquals('Gedmo\Loggable\Entity\LogEntry', $config['logEntryClass']); - $this->assertArrayHasKey('loggable', $config); - $this->assertTrue($config['loggable']); - - $this->assertArrayHasKey('versioned', $config); - $this->assertCount(2, $config['versioned']); - $this->assertContains('title', $config['versioned']); - $this->assertContains('status', $config['versioned']); - } - - public function testLoggableMetadataWithEmbedded() - { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\LoggableWithEmbedded'); - $config = $this->loggable->getConfiguration($this->em, $meta->name); - - $this->assertArrayHasKey('logEntryClass', $config); - $this->assertEquals('Gedmo\Loggable\Entity\LogEntry', $config['logEntryClass']); - $this->assertArrayHasKey('loggable', $config); - $this->assertTrue($config['loggable']); - - $this->assertArrayHasKey('versioned', $config); - $this->assertCount(3, $config['versioned']); - $this->assertContains('title', $config['versioned']); - $this->assertContains('status', $config['versioned']); - $this->assertContains('embedded', $config['versioned']); - } -} diff --git a/tests/Gedmo/Mapping/Xml/MaterializedPathTreeMappingTest.php b/tests/Gedmo/Mapping/Xml/MaterializedPathTreeMappingTest.php index e1e265d11b..a19de177b0 100644 --- a/tests/Gedmo/Mapping/Xml/MaterializedPathTreeMappingTest.php +++ b/tests/Gedmo/Mapping/Xml/MaterializedPathTreeMappingTest.php @@ -1,78 +1,87 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Xml; use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\Common\EventManager; -use Doctrine\ORM\Mapping\Driver\DriverChain; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\ORM\Mapping\Driver\XmlDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\Tests\Mapping\Fixture\Xml\MaterializedPathTree; +use Gedmo\Tests\Tool\BaseTestCaseOM; use Gedmo\Tree\TreeListener; -use Tool\BaseTestCaseOM; /** * These are mapping extension tests * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MaterializedPathTreeMappingTest extends BaseTestCaseOM +final class MaterializedPathTreeMappingTest extends BaseTestCaseOM { - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; + private EntityManager $em; - /** - * @var Gedmo\Tree\TreeListener - */ - private $tree; + private TreeListener $tree; - public function setUp() + protected function setUp(): void { parent::setUp(); - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); + if (PHP_VERSION_ID >= 80000) { + $annotationDriver = new AttributeDriver([]); + } else { + $annotationDriver = new AnnotationDriver(new AnnotationReader()); + } - $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml'); + $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml', XmlDriver::DEFAULT_FILE_EXTENSION, false); - $chain = new DriverChain(); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); - $chain->addDriver($annotationDriver, 'Mapping\Fixture'); + $chain = new MappingDriverChain(); + $chain->addDriver($xmlDriver, 'Gedmo\Tests\Mapping\Fixture\Xml'); + $chain->addDriver($annotationDriver, 'Gedmo\Tests\Mapping\Fixture'); $chain->addDriver($annotationDriver, 'Gedmo\Tree'); $this->tree = new TreeListener(); $this->evm = new EventManager(); $this->evm->addEventSubscriber($this->tree); - $this->em = $this->getMockSqliteEntityManager(array( - 'Mapping\Fixture\Xml\MaterializedPathTree', - ), $chain); + $this->em = $this->getDefaultMockSqliteEntityManager([ + MaterializedPathTree::class, + ], $chain); } - public function testTreeMetadata() + public function testTreeMetadata(): void { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\MaterializedPathTree'); - $config = $this->tree->getConfiguration($this->em, $meta->name); + $meta = $this->em->getClassMetadata(MaterializedPathTree::class); + $config = $this->tree->getConfiguration($this->em, $meta->getName()); - $this->assertArrayHasKey('strategy', $config); - $this->assertEquals('materializedPath', $config['strategy']); - $this->assertArrayHasKey('activate_locking', $config); - $this->assertTrue($config['activate_locking']); - $this->assertArrayHasKey('locking_timeout', $config); - $this->assertEquals(10, $config['locking_timeout']); - $this->assertArrayHasKey('level', $config); - $this->assertEquals('level', $config['level']); - $this->assertArrayHasKey('parent', $config); - $this->assertEquals('parent', $config['parent']); - $this->assertArrayHasKey('path_source', $config); - $this->assertEquals('title', $config['path_source']); - $this->assertArrayHasKey('path', $config); - $this->assertEquals('path', $config['path']); - $this->assertArrayHasKey('lock_time', $config); - $this->assertEquals('lockTime', $config['lock_time']); + static::assertArrayHasKey('strategy', $config); + static::assertSame('materializedPath', $config['strategy']); + static::assertArrayHasKey('activate_locking', $config); + static::assertTrue($config['activate_locking']); + static::assertArrayHasKey('locking_timeout', $config); + static::assertSame(10, $config['locking_timeout']); + static::assertArrayHasKey('level', $config); + static::assertSame('level', $config['level']); + static::assertArrayHasKey('parent', $config); + static::assertSame('parent', $config['parent']); + static::assertArrayHasKey('path_source', $config); + static::assertSame('title', $config['path_source']); + static::assertArrayHasKey('path', $config); + static::assertSame('path', $config['path']); + static::assertArrayHasKey('lock_time', $config); + static::assertSame('lockTime', $config['lock_time']); + static::assertArrayHasKey('path_hash', $config); + static::assertSame('pathHash', $config['path_hash']); } } diff --git a/tests/Gedmo/Mapping/Xml/NestedTreeMappingTest.php b/tests/Gedmo/Mapping/Xml/NestedTreeMappingTest.php index 1f4d5a131a..922e8ffeb6 100644 --- a/tests/Gedmo/Mapping/Xml/NestedTreeMappingTest.php +++ b/tests/Gedmo/Mapping/Xml/NestedTreeMappingTest.php @@ -1,66 +1,69 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Xml; use Doctrine\Common\EventManager; -use Doctrine\ORM\Mapping\Driver\DriverChain; +use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\XmlDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\Tests\Mapping\Fixture\Xml\NestedTree; +use Gedmo\Tests\Tool\BaseTestCaseOM; use Gedmo\Tree\TreeListener; -use Tool\BaseTestCaseOM; /** * These are mapping extension tests * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class NestedTreeMappingTest extends BaseTestCaseOM +final class NestedTreeMappingTest extends BaseTestCaseOM { - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; + private EntityManager $em; - /** - * @var Gedmo\Tree\TreeListener - */ - private $tree; + private TreeListener $tree; - public function setUp() + protected function setUp(): void { parent::setUp(); - $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml'); + $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml', XmlDriver::DEFAULT_FILE_EXTENSION, false); - $chain = new DriverChain(); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); + $chain = new MappingDriverChain(); + $chain->addDriver($xmlDriver, 'Gedmo\Tests\Mapping\Fixture\Xml'); $this->tree = new TreeListener(); $this->evm = new EventManager(); $this->evm->addEventSubscriber($this->tree); - $this->em = $this->getMockSqliteEntityManager(array( - 'Mapping\Fixture\Xml\NestedTree', - ), $chain); + $this->em = $this->getDefaultMockSqliteEntityManager([ + NestedTree::class, + ], $chain); } - public function testTreeMetadata() + public function testTreeMetadata(): void { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\NestedTree'); - $config = $this->tree->getConfiguration($this->em, $meta->name); + $meta = $this->em->getClassMetadata(NestedTree::class); + $config = $this->tree->getConfiguration($this->em, $meta->getName()); - $this->assertArrayHasKey('strategy', $config); - $this->assertEquals('nested', $config['strategy']); - $this->assertArrayHasKey('left', $config); - $this->assertEquals('left', $config['left']); - $this->assertArrayHasKey('right', $config); - $this->assertEquals('right', $config['right']); - $this->assertArrayHasKey('level', $config); - $this->assertEquals('level', $config['level']); - $this->assertArrayHasKey('root', $config); - $this->assertEquals('root', $config['root']); - $this->assertArrayHasKey('parent', $config); - $this->assertEquals('parent', $config['parent']); + static::assertArrayHasKey('strategy', $config); + static::assertSame('nested', $config['strategy']); + static::assertArrayHasKey('left', $config); + static::assertSame('left', $config['left']); + static::assertArrayHasKey('right', $config); + static::assertSame('right', $config['right']); + static::assertArrayHasKey('level', $config); + static::assertSame('level', $config['level']); + static::assertArrayHasKey('root', $config); + static::assertSame('root', $config['root']); + static::assertArrayHasKey('parent', $config); + static::assertSame('parent', $config['parent']); } } diff --git a/tests/Gedmo/Mapping/Xml/ReferencesMappingTest.php b/tests/Gedmo/Mapping/Xml/ReferencesMappingTest.php new file mode 100644 index 0000000000..27a9d3fb96 --- /dev/null +++ b/tests/Gedmo/Mapping/Xml/ReferencesMappingTest.php @@ -0,0 +1,78 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Xml; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\EventManager; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\Mapping\Driver\XmlDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\References\ReferencesListener; +use Gedmo\Tests\Mapping\Fixture\Xml\References; +use Gedmo\Tests\Tool\BaseTestCaseOM; + +/** + * @author Guillermo Fuentes + */ +final class ReferencesMappingTest extends BaseTestCaseOM +{ + private EntityManager $em; + + private ReferencesListener $referencesListener; + + protected function setUp(): void + { + parent::setUp(); + + if (PHP_VERSION_ID >= 80000) { + $annotationDriver = new AttributeDriver([]); + } else { + $annotationDriver = new AnnotationDriver(new AnnotationReader()); + } + + $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml', XmlDriver::DEFAULT_FILE_EXTENSION, false); + + $chain = new MappingDriverChain(); + $chain->addDriver($xmlDriver, 'Gedmo\Tests\Mapping\Fixture\Xml'); + $chain->addDriver($annotationDriver, 'Gedmo\Tests\Mapping\Fixture'); + + $this->referencesListener = new ReferencesListener(); + $this->evm = new EventManager(); + $this->evm->addEventSubscriber($this->referencesListener); + + $this->em = $this->getDefaultMockSqliteEntityManager([ + References::class, + ], $chain); + } + + public function testMetadata(): void + { + $meta = $this->em->getClassMetadata(References::class); + $config = $this->referencesListener->getConfiguration($this->em, $meta->getName()); + + static::assertArrayHasKey('referenceMany', $config); + static::assertArrayHasKey('useObjectClass', $config); + static::assertSame(References::class, $config['useObjectClass']); + $configInternal = $config['referenceMany']; + static::assertArrayHasKey('users', $configInternal); + $configUsers = $configInternal['users']; + static::assertArrayHasKey('field', $configUsers); + static::assertArrayHasKey('type', $configUsers); + static::assertSame('document', $configUsers['type']); + static::assertArrayHasKey('class', $configUsers); + static::assertArrayHasKey('identifier', $configUsers); + static::assertArrayHasKey('mappedBy', $configUsers); + static::assertSame('reference', $configUsers['mappedBy']); + } +} diff --git a/tests/Gedmo/Mapping/Xml/Simplified/TimestampableMappingTest.php b/tests/Gedmo/Mapping/Xml/Simplified/TimestampableMappingTest.php index 7612f7122b..7f73cea63d 100644 --- a/tests/Gedmo/Mapping/Xml/Simplified/TimestampableMappingTest.php +++ b/tests/Gedmo/Mapping/Xml/Simplified/TimestampableMappingTest.php @@ -1,28 +1,35 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Xml\Simplified; use Doctrine\Common\EventManager; -use Doctrine\ORM\Mapping\Driver\DriverChain; use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\Tests\Mapping\Fixture\Xml\Status; +use Gedmo\Tests\Mapping\Fixture\Xml\Timestampable; +use Gedmo\Tests\Tool\BaseTestCaseORM; use Gedmo\Timestampable\TimestampableListener; -use Tool\BaseTestCaseORM; /** * These are mapping extension tests * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TimestampableMappingTest extends BaseTestCaseORM +final class TimestampableMappingTest extends BaseTestCaseORM { - /** - * @var Gedmo\Timestampable\TimestampableListener - */ - private $timestampable; + private TimestampableListener $timestampable; - public function setUp() + protected function setUp(): void { parent::setUp(); @@ -30,43 +37,43 @@ public function setUp() $evm = new EventManager(); $evm->addEventSubscriber($this->timestampable); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - protected function getMetadataDriverImplementation() + public function testTimestampableMetadata(): void { - $xmlDriver = new SimplifiedXmlDriver(array( - __DIR__.'/../../Driver/Xml' => 'Mapping\Fixture\Xml', - )); + $meta = $this->em->getClassMetadata(Timestampable::class); + $config = $this->timestampable->getConfiguration($this->em, $meta->getName()); - $chain = new DriverChain(); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); + static::assertArrayHasKey('create', $config); + static::assertSame('created', $config['create'][0]); + static::assertArrayHasKey('update', $config); + static::assertSame('updated', $config['update'][0]); + static::assertArrayHasKey('change', $config); + $onChange = $config['change'][0]; - return $chain; + static::assertSame('published', $onChange['field']); + static::assertSame('status.title', $onChange['trackedField']); + static::assertSame('Published', $onChange['value']); } - protected function getUsedEntityFixtures() + protected function getMetadataDriverImplementation(): MappingDriver { - return array( - 'Mapping\Fixture\Xml\Timestampable', - 'Mapping\Fixture\Xml\Status', - ); - } + $xmlDriver = new SimplifiedXmlDriver([ + __DIR__.'/../../Driver/Xml' => 'Gedmo\Tests\Mapping\Fixture\Xml', + ], SimplifiedXmlDriver::DEFAULT_FILE_EXTENSION, false); - public function testTimestampableMetadata() - { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\Timestampable'); - $config = $this->timestampable->getConfiguration($this->em, $meta->name); + $chain = new MappingDriverChain(); + $chain->addDriver($xmlDriver, 'Gedmo\Tests\Mapping\Fixture\Xml'); - $this->assertArrayHasKey('create', $config); - $this->assertEquals('created', $config['create'][0]); - $this->assertArrayHasKey('update', $config); - $this->assertEquals('updated', $config['update'][0]); - $this->assertArrayHasKey('change', $config); - $onChange = $config['change'][0]; + return $chain; + } - $this->assertEquals('published', $onChange['field']); - $this->assertEquals('status.title', $onChange['trackedField']); - $this->assertEquals('Published', $onChange['value']); + protected function getUsedEntityFixtures(): array + { + return [ + Timestampable::class, + Status::class, + ]; } } diff --git a/tests/Gedmo/Mapping/Xml/SluggableMappingTest.php b/tests/Gedmo/Mapping/Xml/SluggableMappingTest.php deleted file mode 100644 index d53d3abd1a..0000000000 --- a/tests/Gedmo/Mapping/Xml/SluggableMappingTest.php +++ /dev/null @@ -1,104 +0,0 @@ - - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class SluggableMappingTest extends BaseTestCaseORM -{ - /** - * @var Gedmo\Sluggable\SluggableListener - */ - private $sluggable; - - public function setUp() - { - parent::setUp(); - - $this->sluggable = new SluggableListener(); - $evm = new EventManager(); - $evm->addEventSubscriber($this->sluggable); - - $this->getMockSqliteEntityManager($evm); - } - - protected function getUsedEntityFixtures() - { - return array('Mapping\Fixture\Xml\Sluggable'); - } - - protected function getMetadataDriverImplementation() - { - $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml'); - - $chain = new DriverChain(); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); - - return $chain; - } - - /** - * @test - */ - public function shouldBeAbleToMapSluggableMetadata() - { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\Sluggable'); - $config = $this->sluggable->getConfiguration($this->em, $meta->name); - - $this->assertArrayHasKey('slug', $config['slugs']); - $this->assertCount(1, $config['slugs']); - $config = $config['slugs']['slug']; - - $this->assertEquals('slug', $config['slug']); - $this->assertArrayHasKey('style', $config); - $this->assertEquals('camel', $config['style']); - $this->assertArrayHasKey('updatable', $config); - $this->assertFalse($config['updatable']); - $this->assertArrayHasKey('unique', $config); - $this->assertTrue($config['unique']); - $this->assertArrayHasKey('separator', $config); - $this->assertEquals('_', $config['separator']); - - $this->assertArrayHasKey('fields', $config); - $this->assertCount(3, $config['fields']); - $fields = $config['fields']; - - $this->assertEquals('title', $fields[0]); - $this->assertEquals('ean', $fields[1]); - $this->assertEquals('code', $fields[2]); - - $this->assertArrayHasKey('handlers', $config); - $this->assertEquals(2, count($config['handlers'])); - $handlers = $config['handlers']; - - $this->assertArrayHasKey('Gedmo\Sluggable\Handler\TreeSlugHandler', $handlers); - $this->assertArrayHasKey('Gedmo\Sluggable\Handler\RelativeSlugHandler', $handlers); - - $first = $handlers['Gedmo\Sluggable\Handler\TreeSlugHandler']; - $this->assertEquals(2, count($first)); - $this->assertArrayHasKey('parentRelationField', $first); - $this->assertArrayHasKey('separator', $first); - $this->assertEquals('parent', $first['parentRelationField']); - $this->assertEquals('/', $first['separator']); - - $second = $handlers['Gedmo\Sluggable\Handler\RelativeSlugHandler']; - $this->assertEquals(3, count($second)); - $this->assertArrayHasKey('relationField', $second); - $this->assertArrayHasKey('relationSlugField', $second); - $this->assertArrayHasKey('separator', $second); - $this->assertEquals('parent', $second['relationField']); - $this->assertEquals('test', $second['relationSlugField']); - $this->assertEquals('-', $second['separator']); - } -} diff --git a/tests/Gedmo/Mapping/Xml/SoftDeleteableMappingTest.php b/tests/Gedmo/Mapping/Xml/SoftDeleteableMappingTest.php deleted file mode 100644 index 769b054ba8..0000000000 --- a/tests/Gedmo/Mapping/Xml/SoftDeleteableMappingTest.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class SoftDeleteableMappingTest extends BaseTestCaseOM -{ - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; - - /** - * @var Gedmo\SoftDeleteable\SoftDeleteableListener - */ - private $softDeleteable; - - public function setUp() - { - parent::setUp(); - - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); - - $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml'); - - $chain = new DriverChain(); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); - $chain->addDriver($annotationDriver, 'Mapping\Fixture'); - - $this->softDeleteable = new SoftDeleteableListener(); - $this->evm = new EventManager(); - $this->evm->addEventSubscriber($this->softDeleteable); - - $this->em = $this->getMockSqliteEntityManager(array( - 'Mapping\Fixture\Xml\SoftDeleteable', - 'Mapping\Fixture\SoftDeleteable', - ), $chain); - } - - public function testMetadata() - { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\SoftDeleteable'); - $config = $this->softDeleteable->getConfiguration($this->em, $meta->name); - - $this->assertArrayHasKey('softDeleteable', $config); - $this->assertTrue($config['softDeleteable']); - $this->assertArrayHasKey('timeAware', $config); - $this->assertFalse($config['timeAware']); - $this->assertArrayHasKey('fieldName', $config); - $this->assertEquals('deletedAt', $config['fieldName']); - } -} diff --git a/tests/Gedmo/Mapping/Xml/SortableMappingTest.php b/tests/Gedmo/Mapping/Xml/SortableMappingTest.php deleted file mode 100644 index 4ca128c678..0000000000 --- a/tests/Gedmo/Mapping/Xml/SortableMappingTest.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class SortableMappingTest extends BaseTestCaseOM -{ - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; - - /** - * @var Gedmo\Sortable\SortableListener - */ - private $sortable; - - public function setUp() - { - parent::setUp(); - - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); - - $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml'); - - $chain = new DriverChain(); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); - $chain->addDriver($annotationDriver, 'Mapping\Fixture'); - - $this->sortable = new SortableListener(); - $this->evm = new EventManager(); - $this->evm->addEventSubscriber($this->sortable); - - $this->em = $this->getMockSqliteEntityManager(array( - 'Mapping\Fixture\Xml\Sortable', - 'Mapping\Fixture\SortableGroup', - ), $chain); - } - - public function testSluggableMetadata() - { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\Sortable'); - $config = $this->sortable->getConfiguration($this->em, $meta->name); - - $this->assertArrayHasKey('position', $config); - $this->assertEquals('position', $config['position']); - $this->assertArrayHasKey('groups', $config); - $this->assertCount(3, $config['groups']); - $this->assertEquals('grouping', $config['groups'][0]); - $this->assertEquals('sortable_group', $config['groups'][1]); - $this->assertEquals('sortable_groups', $config['groups'][2]); - } -} diff --git a/tests/Gedmo/Mapping/Xml/TimestampableMappingTest.php b/tests/Gedmo/Mapping/Xml/TimestampableMappingTest.php deleted file mode 100644 index dad4858eee..0000000000 --- a/tests/Gedmo/Mapping/Xml/TimestampableMappingTest.php +++ /dev/null @@ -1,65 +0,0 @@ - - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class TimestampableMappingTest extends BaseTestCaseOM -{ - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; - - /** - * @var Gedmo\Timestampable\TimestampableListener - */ - private $timestampable; - - public function setUp() - { - parent::setUp(); - - $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml'); - - $chain = new DriverChain(); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); - - $this->timestampable = new TimestampableListener(); - $this->evm = new EventManager(); - $this->evm->addEventSubscriber($this->timestampable); - - $this->em = $this->getMockSqliteEntityManager(array( - 'Mapping\Fixture\Xml\Timestampable', - 'Mapping\Fixture\Xml\Status', - ), $chain); - } - - public function testTimestampableMetadata() - { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\Timestampable'); - $config = $this->timestampable->getConfiguration($this->em, $meta->name); - - $this->assertArrayHasKey('create', $config); - $this->assertEquals('created', $config['create'][0]); - $this->assertArrayHasKey('update', $config); - $this->assertEquals('updated', $config['update'][0]); - $this->assertArrayHasKey('change', $config); - $onChange = $config['change'][0]; - - $this->assertEquals('published', $onChange['field']); - $this->assertEquals('status.title', $onChange['trackedField']); - $this->assertEquals('Published', $onChange['value']); - } -} diff --git a/tests/Gedmo/Mapping/Xml/TranslatableMappingTest.php b/tests/Gedmo/Mapping/Xml/TranslatableMappingTest.php index 22e6b6de63..3c58482c5d 100644 --- a/tests/Gedmo/Mapping/Xml/TranslatableMappingTest.php +++ b/tests/Gedmo/Mapping/Xml/TranslatableMappingTest.php @@ -1,82 +1,91 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Mapping\Xml; -use Doctrine\Common\EventManager; use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\ORM\Mapping\Driver\DriverChain; -use Doctrine\ORM\Mapping\Driver\XmlDriver; +use Doctrine\Common\EventManager; +use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\Mapping\Driver\XmlDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\Tests\Mapping\Fixture\Xml\Translatable; +use Gedmo\Tests\Mapping\Fixture\Xml\TranslatableWithEmbedded; +use Gedmo\Tests\Tool\BaseTestCaseOM; +use Gedmo\Translatable\Entity\Translation; use Gedmo\Translatable\TranslatableListener; -use Tool\BaseTestCaseOM; /** * These are mapping extension tests * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableMappingTest extends BaseTestCaseOM +final class TranslatableMappingTest extends BaseTestCaseOM { - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; + private EntityManager $em; - /** - * @var Gedmo\Translatable\TranslatableListener - */ - private $translatable; + private TranslatableListener $translatable; - public function setUp() + protected function setUp(): void { parent::setUp(); - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); + if (PHP_VERSION_ID >= 80000) { + $annotationDriver = new AttributeDriver([]); + } else { + $annotationDriver = new AnnotationDriver(new AnnotationReader()); + } - $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml'); + $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml', XmlDriver::DEFAULT_FILE_EXTENSION, false); - $chain = new DriverChain(); + $chain = new MappingDriverChain(); $chain->addDriver($annotationDriver, 'Gedmo\Translatable'); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); + $chain->addDriver($xmlDriver, 'Gedmo\Tests\Mapping\Fixture\Xml'); $this->translatable = new TranslatableListener(); $this->evm = new EventManager(); $this->evm->addEventSubscriber($this->translatable); - $this->em = $this->getMockSqliteEntityManager(array( - 'Gedmo\Translatable\Entity\Translation', - 'Mapping\Fixture\Xml\Translatable', - ), $chain); + $this->em = $this->getDefaultMockSqliteEntityManager([ + Translation::class, + Translatable::class, + ], $chain); } - public function testTranslatableMetadata() + public function testTranslatableMetadata(): void { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\Translatable'); - $config = $this->translatable->getConfiguration($this->em, $meta->name); - - $this->assertArrayHasKey('translationClass', $config); - $this->assertEquals('Gedmo\Translatable\Entity\Translation', $config['translationClass']); - $this->assertArrayHasKey('locale', $config); - $this->assertEquals('locale', $config['locale']); - - $this->assertArrayHasKey('fields', $config); - $this->assertCount(4, $config['fields']); - $this->assertTrue(in_array('title', $config['fields'])); - $this->assertTrue(in_array('content', $config['fields'])); - $this->assertTrue(in_array('author', $config['fields'])); - $this->assertTrue(in_array('views', $config['fields'])); - $this->assertTrue($config['fallback']['author']); - $this->assertFalse($config['fallback']['views']); + $meta = $this->em->getClassMetadata(Translatable::class); + $config = $this->translatable->getConfiguration($this->em, $meta->getName()); + + static::assertArrayHasKey('translationClass', $config); + static::assertSame(Translation::class, $config['translationClass']); + static::assertArrayHasKey('locale', $config); + static::assertSame('locale', $config['locale']); + + static::assertArrayHasKey('fields', $config); + static::assertCount(4, $config['fields']); + static::assertContains('title', $config['fields']); + static::assertContains('content', $config['fields']); + static::assertContains('author', $config['fields']); + static::assertContains('views', $config['fields']); + static::assertTrue($config['fallback']['author']); + static::assertFalse($config['fallback']['views']); } - public function testTranslatableMetadataWithEmbedded() + public function testTranslatableMetadataWithEmbedded(): void { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\TranslatableWithEmbedded'); - $config = $this->translatable->getConfiguration($this->em, $meta->name); + $meta = $this->em->getClassMetadata(TranslatableWithEmbedded::class); + $config = $this->translatable->getConfiguration($this->em, $meta->getName()); - $this->assertContains('embedded.subtitle', $config['fields']); + static::assertContains('embedded.subtitle', $config['fields']); } } diff --git a/tests/Gedmo/Mapping/Xml/UploadableMappingTest.php b/tests/Gedmo/Mapping/Xml/UploadableMappingTest.php deleted file mode 100644 index 8096b83a05..0000000000 --- a/tests/Gedmo/Mapping/Xml/UploadableMappingTest.php +++ /dev/null @@ -1,79 +0,0 @@ - - * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class UploadableMappingTest extends BaseTestCaseOM -{ - /** - * @var Doctrine\ORM\EntityManager - */ - private $em; - - /** - * @var Gedmo\SoftDeleteable\UploadableListener - */ - private $listener; - - public function setUp() - { - parent::setUp(); - - Validator::$enableMimeTypesConfigException = false; - - $reader = new AnnotationReader(); - $annotationDriver = new AnnotationDriver($reader); - - $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml'); - - $chain = new DriverChain(); - $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml'); - $chain->addDriver($annotationDriver, 'Mapping\Fixture'); - - $this->listener = new UploadableListener(); - $this->evm = new EventManager(); - $this->evm->addEventSubscriber($this->listener); - - $this->em = $this->getMockSqliteEntityManager(array( - 'Mapping\Fixture\Xml\Uploadable', - ), $chain); - } - - public function testMetadata() - { - $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\Uploadable'); - $config = $this->listener->getConfiguration($this->em, $meta->name); - - $this->assertTrue($config['uploadable']); - $this->assertTrue($config['allowOverwrite']); - $this->assertTrue($config['appendNumber']); - $this->assertEquals('/my/path', $config['path']); - $this->assertEquals('getPath', $config['pathMethod']); - $this->assertEquals('mimeType', $config['fileMimeTypeField']); - $this->assertEquals('path', $config['filePathField']); - $this->assertEquals('size', $config['fileSizeField']); - $this->assertEquals('callbackMethod', $config['callback']); - $this->assertEquals('SHA1', $config['filenameGenerator']); - $this->assertEquals(1500, $config['maxSize']); - $this->assertContains('text/plain', $config['allowedTypes']); - $this->assertContains('text/css', $config['allowedTypes']); - $this->assertContains('video/jpeg', $config['disallowedTypes']); - $this->assertContains('text/html', $config['disallowedTypes']); - } -} diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyNullify/Article.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyNullify/Article.php index e77caa54da..2c85195668 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyNullify/Article.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyNullify/Article.php @@ -1,66 +1,66 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyNullify; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceOne(targetDocument="Type", simple="true", inversedBy="articles") - * @var Type + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyNullify\Type", inversedBy="articles") */ - private $type; + #[ODM\ReferenceOne(targetDocument: Type::class, inversedBy: 'articles')] + private ?Type $type = null; - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param Type $type - */ - public function setType(Type $type) + public function setType(?Type $type): void { $this->type = $type; } - /** - * @return Type - */ - public function getType() + public function getType(): ?Type { return $this->type; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyNullify/Type.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyNullify/Type.php index 1553b094e4..fc8f1ecad8 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyNullify/Type.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyNullify/Type.php @@ -1,99 +1,98 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyNullify; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="types") */ +#[ODM\Document(collection: 'types')] class Type { /** - * @ODM\Id + * @var Collection + * + * @ODM\ReferenceMany(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyNullify\Article", mappedBy="type") + * + * @Gedmo\ReferenceIntegrity("nullify") */ - private $id; + #[ODM\ReferenceMany(targetDocument: Article::class, mappedBy: 'type')] + #[Gedmo\ReferenceIntegrity(value: 'nullify')] + protected $articles; /** - * @ODM\String + * @var string|null + * + * @ODM\Id */ - private $title; + #[ODM\Id] + private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $identifier; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceMany(targetDocument="Article", mappedBy="type") - * @Gedmo\ReferenceIntegrity("nullify") - * @var ArrayCollection + * @ODM\Field(type="string") */ - protected $articles = array(); + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $identifier = null; public function __construct() { $this->articles = new ArrayCollection(); } - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param string $identifier - */ - public function setIdentifier($identifier) + public function setIdentifier(?string $identifier): void { $this->identifier = $identifier; } - /** - * @return string - */ - public function getIdentifier() + public function getIdentifier(): ?string { return $this->identifier; } - /** - * Add articles - * - * @param Article $article - */ - public function addArticle(Article $article) + public function addArticle(Article $article): void { $this->articles[] = $article; } /** - * Get posts - * - * @return ArrayCollection $articles + * @return Collection $articles */ - public function getArticles() + public function getArticles(): Collection { return $this->articles; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyPull/Article.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyPull/Article.php index ee93a0cf80..6a4562446d 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyPull/Article.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyPull/Article.php @@ -1,76 +1,78 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyPull; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceMany(targetDocument="Type", simple="true", inversedBy="articles") - * @var ArrayCollection + * @var Collection + * + * @ODM\ReferenceMany(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyPull\Type", inversedBy="articles") */ + #[ODM\ReferenceMany(targetDocument: Type::class, inversedBy: 'articles')] private $types; - + public function __construct() { $this->types = new ArrayCollection(); } - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * Add types - * - * @param Type $type - */ - public function addType(Type $type) + public function addType(Type $type): void { $this->types[] = $type; } /** - * Get posts - * - * @return ArrayCollection $types + * @return Collection */ - public function getTypes() + public function getTypes(): Collection { return $this->types; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyPull/Type.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyPull/Type.php index e2ef29dbaf..132ec46754 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyPull/Type.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyPull/Type.php @@ -1,99 +1,98 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyPull; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="types") */ +#[ODM\Document(collection: 'types')] class Type { /** - * @ODM\Id + * @var Collection + * + * @ODM\ReferenceMany(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyPull\Article", mappedBy="types") + * + * @Gedmo\ReferenceIntegrity("pull") */ - private $id; + #[ODM\ReferenceMany(targetDocument: Article::class, mappedBy: 'types')] + #[Gedmo\ReferenceIntegrity(value: 'pull')] + protected $articles; /** - * @ODM\String + * @var string|null + * + * @ODM\Id */ - private $title; + #[ODM\Id] + private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $identifier; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceMany(targetDocument="Article", mappedBy="types") - * @Gedmo\ReferenceIntegrity("pull") - * @var ArrayCollection + * @ODM\Field(type="string") */ - protected $articles = array(); + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $identifier = null; public function __construct() { $this->articles = new ArrayCollection(); } - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param string $identifier - */ - public function setIdentifier($identifier) + public function setIdentifier(?string $identifier): void { $this->identifier = $identifier; } - /** - * @return string - */ - public function getIdentifier() + public function getIdentifier(): ?string { return $this->identifier; } - /** - * Add articles - * - * @param Article $article - */ - public function addArticle(Article $article) + public function addArticle(Article $article): void { $this->articles[] = $article; } /** - * Get posts - * - * @return ArrayCollection $articles + * @return Collection $articles */ - public function getArticles() + public function getArticles(): Collection { return $this->articles; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyRestrict/Article.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyRestrict/Article.php index 9b7527ec47..02b237da59 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyRestrict/Article.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyRestrict/Article.php @@ -1,66 +1,66 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyRestrict; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceOne(targetDocument="Type", simple="true", inversedBy="articles") - * @var Type + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyRestrict\Type", inversedBy="articles") */ - private $type; + #[ODM\ReferenceOne(targetDocument: Type::class, inversedBy: 'articles')] + private ?Type $type = null; - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param Type $type - */ - public function setType(Type $type) + public function setType(Type $type): void { $this->type = $type; } - /** - * @return Type - */ - public function getType() + public function getType(): ?Type { return $this->type; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyRestrict/Type.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyRestrict/Type.php index 4eec3f7deb..92cd0f80f1 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyRestrict/Type.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/ManyRestrict/Type.php @@ -1,99 +1,98 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyRestrict; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="types") */ +#[ODM\Document(collection: 'types')] class Type { /** - * @ODM\Id + * @var Collection + * + * @ODM\ReferenceMany(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyRestrict\Article", mappedBy="type") + * + * @Gedmo\ReferenceIntegrity("restrict") */ - private $id; + #[ODM\ReferenceMany(targetDocument: Article::class, mappedBy: 'type')] + #[Gedmo\ReferenceIntegrity(value: 'restrict')] + protected $articles; /** - * @ODM\String + * @var string|null + * + * @ODM\Id */ - private $title; + #[ODM\Id] + private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $identifier; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceMany(targetDocument="Article", mappedBy="type") - * @Gedmo\ReferenceIntegrity("restrict") - * @var ArrayCollection + * @ODM\Field(type="string") */ - protected $articles = array(); + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $identifier = null; public function __construct() { $this->articles = new ArrayCollection(); } - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param string $identifier - */ - public function setIdentifier($identifier) + public function setIdentifier(?string $identifier): void { $this->identifier = $identifier; } - /** - * @return string - */ - public function getIdentifier() + public function getIdentifier(): ?string { return $this->identifier; } - /** - * Add articles - * - * @param Article $article - */ - public function addArticle(Article $article) + public function addArticle(Article $article): void { $this->articles[] = $article; } /** - * Get posts - * - * @return ArrayCollection $articles + * @return Collection $articles */ - public function getArticles() + public function getArticles(): Collection { return $this->articles; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneNullify/Article.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneNullify/Article.php index 7bea53818c..3f979db0db 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneNullify/Article.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneNullify/Article.php @@ -1,66 +1,66 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneNullify; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceOne(targetDocument="Type", simple="true", inversedBy="articles") - * @var Type + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneNullify\Type", inversedBy="articles") */ - private $type; + #[ODM\ReferenceOne(targetDocument: Type::class, inversedBy: 'articles')] + private ?Type $type = null; - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param Type $type - */ - public function setType(Type $type) + public function setType(?Type $type): void { $this->type = $type; } - /** - * @return Type - */ - public function getType() + public function getType(): ?Type { return $this->type; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneNullify/Type.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneNullify/Type.php index cc70d50d46..3ee2dde2cf 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneNullify/Type.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneNullify/Type.php @@ -1,89 +1,88 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneNullify; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="types") */ +#[ODM\Document(collection: 'types')] class Type { /** - * @ODM\Id + * @var Article + * + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneNullify\Article", mappedBy="type") + * + * @Gedmo\ReferenceIntegrity("nullify") */ - private $id; + #[ODM\ReferenceOne(targetDocument: Article::class, mappedBy: 'type')] + #[Gedmo\ReferenceIntegrity(value: 'nullify')] + protected $article; /** - * @ODM\String + * @var string|null + * + * @ODM\Id */ - private $title; + #[ODM\Id] + private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $identifier; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceOne(targetDocument="Article", mappedBy="type") - * @Gedmo\ReferenceIntegrity("nullify") - * @var Article + * @ODM\Field(type="string") */ - protected $article; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $identifier = null; - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param string $identifier - */ - public function setIdentifier($identifier) + public function setIdentifier(?string $identifier): void { $this->identifier = $identifier; } - /** - * @return string - */ - public function getIdentifier() + public function getIdentifier(): ?string { return $this->identifier; } - /** - * @param Article $article - */ - public function setArticle(Article $article) + public function setArticle(?Article $article): void { $this->article = $article; } - /** - * @return Article $article - */ - public function getArticle() + public function getArticle(): ?Article { return $this->article; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OnePull/Article.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OnePull/Article.php index ec2b76bd9a..c66bc710dd 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OnePull/Article.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OnePull/Article.php @@ -1,76 +1,78 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OnePull; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceMany(targetDocument="Type", simple="true", inversedBy="articles") - * @var ArrayCollection + * @var Collection + * + * @ODM\ReferenceMany(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OnePull\Type", inversedBy="articles") */ + #[ODM\ReferenceMany(targetDocument: Type::class, inversedBy: 'articles')] private $types; - + public function __construct() { $this->types = new ArrayCollection(); } - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * Add types - * - * @param Type $type - */ - public function addType(Type $type) + public function addType(Type $type): void { $this->types[] = $type; } /** - * Get posts - * - * @return ArrayCollection $types + * @return Collection */ - public function getTypes() + public function getTypes(): Collection { return $this->types; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OnePull/Type.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OnePull/Type.php index 977225db0d..cf6cb2737f 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OnePull/Type.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OnePull/Type.php @@ -1,89 +1,88 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OnePull; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="types") */ +#[ODM\Document(collection: 'types')] class Type { /** - * @ODM\Id + * @var Article|null + * + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OnePull\Article", mappedBy="types") + * + * @Gedmo\ReferenceIntegrity("pull") */ - private $id; + #[ODM\ReferenceOne(targetDocument: Article::class, mappedBy: 'types')] + #[Gedmo\ReferenceIntegrity(value: 'pull')] + protected $article; /** - * @ODM\String + * @var string|null + * + * @ODM\Id */ - private $title; + #[ODM\Id] + private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $identifier; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceOne(targetDocument="Article", mappedBy="types") - * @Gedmo\ReferenceIntegrity("pull") - * @var Article + * @ODM\Field(type="string") */ - protected $article; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $identifier = null; - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param string $identifier - */ - public function setIdentifier($identifier) + public function setIdentifier(?string $identifier): void { $this->identifier = $identifier; } - /** - * @return string - */ - public function getIdentifier() + public function getIdentifier(): ?string { return $this->identifier; } - /** - * @param Article $article - */ - public function setArticle(Article $article) + public function setArticle(?Article $article): void { $this->article = $article; } - /** - * @return Article $article - */ - public function getArticle() + public function getArticle(): ?Article { return $this->article; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneRestrict/Article.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneRestrict/Article.php index fb86cd92e2..0a0c1008a2 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneRestrict/Article.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneRestrict/Article.php @@ -1,66 +1,66 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneRestrict; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceOne(targetDocument="Type", simple="true", inversedBy="articles") - * @var Type + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneRestrict\Type", inversedBy="articles") */ - private $type; + #[ODM\ReferenceOne(targetDocument: Type::class, inversedBy: 'articles')] + private ?Type $type = null; - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param Type $type - */ - public function setType(Type $type) + public function setType(?Type $type): void { $this->type = $type; } - /** - * @return Type - */ - public function getType() + public function getType(): ?Type { return $this->type; } diff --git a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneRestrict/Type.php b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneRestrict/Type.php index e7007f2ddf..62bd70b315 100644 --- a/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneRestrict/Type.php +++ b/tests/Gedmo/ReferenceIntegrity/Fixture/Document/OneRestrict/Type.php @@ -1,89 +1,88 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneRestrict; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="types") */ +#[ODM\Document(collection: 'types')] class Type { /** - * @ODM\Id + * @var Article|null + * + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneRestrict\Article", mappedBy="type") + * + * @Gedmo\ReferenceIntegrity("restrict") */ - private $id; + #[ODM\ReferenceOne(targetDocument: Article::class, mappedBy: 'type')] + #[Gedmo\ReferenceIntegrity(value: 'restrict')] + protected $article; /** - * @ODM\String + * @var string|null + * + * @ODM\Id */ - private $title; + #[ODM\Id] + private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $identifier; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceOne(targetDocument="Article", mappedBy="type") - * @Gedmo\ReferenceIntegrity("restrict") - * @var Article + * @ODM\Field(type="string") */ - protected $article; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $identifier = null; - /** - * @return mixed - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param string $identifier - */ - public function setIdentifier($identifier) + public function setIdentifier(?string $identifier): void { $this->identifier = $identifier; } - /** - * @return string - */ - public function getIdentifier() + public function getIdentifier(): ?string { return $this->identifier; } - /** - * @param Article $article - */ - public function setArticle(Article $article) + public function setArticle(?Article $article): void { $this->article = $article; } - /** - * @return Article $articles - */ - public function getArticle() + public function getArticle(): ?Article { return $this->article; } diff --git a/tests/Gedmo/ReferenceIntegrity/ReferenceIntegrityDocumentTest.php b/tests/Gedmo/ReferenceIntegrity/ReferenceIntegrityDocumentTest.php index 4cd1fd6c61..1964011778 100644 --- a/tests/Gedmo/ReferenceIntegrity/ReferenceIntegrityDocumentTest.php +++ b/tests/Gedmo/ReferenceIntegrity/ReferenceIntegrityDocumentTest.php @@ -1,44 +1,48 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\ReferenceIntegrity; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseMongoODM; +use Gedmo\Exception\ReferenceIntegrityStrictException; +use Gedmo\ReferenceIntegrity\ReferenceIntegrityListener; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyNullify\Article as ArticleManyNullify; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyNullify\Type as TypeManyNullify; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyPull\Article as ArticleManyPull; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyPull\Type as TypeManyPull; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyRestrict\Article; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\ManyRestrict\Type; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneNullify\Article as ArticleOneNullify; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneNullify\Type as TypeOneNullify; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OnePull\Article as ArticleOnePull; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OnePull\Type as TypeOnePull; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneRestrict\Article as ArticleOneRestrict; +use Gedmo\Tests\ReferenceIntegrity\Fixture\Document\OneRestrict\Type as TypeOneRestrict; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for the ReferenceIntegrity extension * * @author Evert Harmeling - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ReferenceIntegrityDocumentTest extends BaseTestCaseMongoODM +final class ReferenceIntegrityDocumentTest extends BaseTestCaseMongoODM { - const TYPE_ONE_NULLIFY_CLASS = 'ReferenceIntegrity\Fixture\Document\OneNullify\Type'; - const ARTICLE_ONE_NULLIFY_CLASS = 'ReferenceIntegrity\Fixture\Document\OneNullify\Article'; - - const TYPE_MANY_NULLIFY_CLASS = 'ReferenceIntegrity\Fixture\Document\ManyNullify\Type'; - const ARTICLE_MANY_NULLIFY_CLASS = 'ReferenceIntegrity\Fixture\Document\ManyNullify\Article'; - - const TYPE_ONE_PULL_CLASS = 'ReferenceIntegrity\Fixture\Document\OnePull\Type'; - const ARTICLE_ONE_PULL_CLASS = 'ReferenceIntegrity\Fixture\Document\OnePull\Article'; - - const TYPE_MANY_PULL_CLASS = 'ReferenceIntegrity\Fixture\Document\ManyPull\Type'; - const ARTICLE_MANY_PULL_CLASS = 'ReferenceIntegrity\Fixture\Document\ManyPull\Article'; - - const TYPE_ONE_RESTRICT_CLASS = 'ReferenceIntegrity\Fixture\Document\OneRestrict\Type'; - const ARTICLE_ONE_RESTRICT_CLASS = 'ReferenceIntegrity\Fixture\Document\OneRestrict\Article'; - - const TYPE_MANY_RESTRICT_CLASS = 'ReferenceIntegrity\Fixture\Document\ManyRestrict\Type'; - const ARTICLE_MANY_RESTRICT_CLASS = 'ReferenceIntegrity\Fixture\Document\ManyRestrict\Article'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new ReferenceIntegrityListener()); - $this->dm = $this->getMockDocumentManager($evm, $this->getMockAnnotatedConfig()); + $this->dm = $this->getDefaultDocumentManager($evm); $this->populateOneNullify(); $this->populateManyNullify(); @@ -50,152 +54,144 @@ protected function setUp() $this->populateManyRestrict(); } - public function testOneNullify() + public function testOneNullify(): void { - $type = $this->dm->getRepository(self::TYPE_ONE_NULLIFY_CLASS) - ->findOneByTitle('One Nullify Type'); + $type = $this->dm->getRepository(TypeOneNullify::class) + ->findOneBy(['title' => 'One Nullify Type']); - $this->assertFalse(is_null($type)); - $this->assertTrue(is_object($type)); + static::assertNotNull($type); + static::assertIsObject($type); $this->dm->remove($type); $this->dm->flush(); - $type = $this->dm->getRepository(self::TYPE_ONE_NULLIFY_CLASS) - ->findOneByTitle('One Nullify Type'); - $this->assertNull($type); + $type = $this->dm->getRepository(TypeOneNullify::class) + ->findOneBy(['title' => 'One Nullify Type']); + static::assertNull($type); - $article = $this->dm->getRepository(self::ARTICLE_ONE_NULLIFY_CLASS) - ->findOneByTitle('One Nullify Article'); + $article = $this->dm->getRepository(ArticleOneNullify::class) + ->findOneBy(['title' => 'One Nullify Article']); - $this->assertNull($article->getType()); + static::assertNull($article->getType()); $this->dm->clear(); } - public function testManyNullify() + public function testManyNullify(): void { - $type = $this->dm->getRepository(self::TYPE_MANY_NULLIFY_CLASS) - ->findOneByTitle('Many Nullify Type'); + $type = $this->dm->getRepository(TypeManyNullify::class) + ->findOneBy(['title' => 'Many Nullify Type']); - $this->assertFalse(is_null($type)); - $this->assertTrue(is_object($type)); + static::assertNotNull($type); + static::assertIsObject($type); $this->dm->remove($type); $this->dm->flush(); - $type = $this->dm->getRepository(self::TYPE_MANY_NULLIFY_CLASS) - ->findOneByTitle('Many Nullify Type'); - $this->assertNull($type); + $type = $this->dm->getRepository(TypeManyNullify::class) + ->findOneBy(['title' => 'Many Nullify Type']); + static::assertNull($type); - $article = $this->dm->getRepository(self::ARTICLE_MANY_NULLIFY_CLASS) - ->findOneByTitle('Many Nullify Article'); + $article = $this->dm->getRepository(ArticleManyNullify::class) + ->findOneBy(['title' => 'Many Nullify Article']); - $this->assertNull($article->getType()); + static::assertNull($article->getType()); $this->dm->clear(); } - public function testOnePull() + public function testOnePull(): void { - $type1 = $this->dm->getRepository(self::TYPE_ONE_PULL_CLASS) - ->findOneByTitle('One Pull Type 1'); - $type2 = $this->dm->getRepository(self::TYPE_ONE_PULL_CLASS) - ->findOneByTitle('One Pull Type 2'); + $type1 = $this->dm->getRepository(TypeOnePull::class) + ->findOneBy(['title' => 'One Pull Type 1']); + $type2 = $this->dm->getRepository(TypeOnePull::class) + ->findOneBy(['title' => 'One Pull Type 2']); - $this->assertFalse(is_null($type1)); - $this->assertTrue(is_object($type1)); + static::assertNotNull($type1); + static::assertIsObject($type1); - $this->assertFalse(is_null($type2)); - $this->assertTrue(is_object($type2)); + static::assertNotNull($type2); + static::assertIsObject($type2); $this->dm->remove($type2); $this->dm->flush(); - $type2 = $this->dm->getRepository(self::TYPE_ONE_PULL_CLASS) - ->findOneByTitle('One Pull Type 2'); - $this->assertNull($type2); + $type2 = $this->dm->getRepository(TypeOnePull::class) + ->findOneBy(['title' => 'One Pull Type 2']); + static::assertNull($type2); + + $article = $this->dm->getRepository(ArticleOnePull::class) + ->findOneBy(['title' => 'One Pull Article']); - $article = $this->dm->getRepository(self::ARTICLE_ONE_PULL_CLASS) - ->findOneByTitle('One Pull Article'); - $types = $article->getTypes(); - $this->assertTrue(count($types)===1); - $this->assertEquals('One Pull Type 1',$types[0]->getTitle()); - + static::assertCount(1, $types); + static::assertSame('One Pull Type 1', $types[0]->getTitle()); + $this->dm->clear(); } - public function testManyPull() + public function testManyPull(): void { - $type1 = $this->dm->getRepository(self::TYPE_ONE_PULL_CLASS) - ->findOneByTitle('Many Pull Type 1'); - $type2 = $this->dm->getRepository(self::TYPE_ONE_PULL_CLASS) - ->findOneByTitle('Many Pull Type 2'); + $type1 = $this->dm->getRepository(TypeOnePull::class) + ->findOneBy(['title' => 'Many Pull Type 1']); + $type2 = $this->dm->getRepository(TypeOnePull::class) + ->findOneBy(['title' => 'Many Pull Type 2']); - $this->assertFalse(is_null($type1)); - $this->assertTrue(is_object($type1)); + static::assertNotNull($type1); + static::assertIsObject($type1); - $this->assertFalse(is_null($type2)); - $this->assertTrue(is_object($type2)); + static::assertNotNull($type2); + static::assertIsObject($type2); $this->dm->remove($type2); $this->dm->flush(); - $type2 = $this->dm->getRepository(self::TYPE_MANY_PULL_CLASS) - ->findOneByTitle('Many Pull Type 2'); - $this->assertNull($type2); + $type2 = $this->dm->getRepository(TypeManyPull::class) + ->findOneBy(['title' => 'Many Pull Type 2']); + static::assertNull($type2); + + $article = $this->dm->getRepository(ArticleManyPull::class) + ->findOneBy(['title' => 'Many Pull Article']); - $article = $this->dm->getRepository(self::ARTICLE_MANY_PULL_CLASS) - ->findOneByTitle('Many Pull Article'); - $types = $article->getTypes(); - $this->assertTrue(count($types)===1); - $this->assertEquals('Many Pull Type 1',$types[0]->getTitle()); + static::assertCount(1, $types); + static::assertSame('Many Pull Type 1', $types[0]->getTitle()); $this->dm->clear(); } - /** - * @test - * @expectedException Gedmo\Exception\ReferenceIntegrityStrictException - */ - public function testOneRestrict() + public function testOneRestrict(): void { - $type = $this->dm->getRepository(self::TYPE_ONE_RESTRICT_CLASS) - ->findOneByTitle('One Restrict Type'); + $this->expectException(ReferenceIntegrityStrictException::class); + $type = $this->dm->getRepository(TypeOneRestrict::class) + ->findOneBy(['title' => 'One Restrict Type']); - $this->assertFalse(is_null($type)); - $this->assertTrue(is_object($type)); + static::assertNotNull($type); + static::assertIsObject($type); $this->dm->remove($type); $this->dm->flush(); } - /** - * @test - * @expectedException Gedmo\Exception\ReferenceIntegrityStrictException - */ - public function testManyRestrict() + public function testManyRestrict(): void { - $type = $this->dm->getRepository(self::TYPE_MANY_RESTRICT_CLASS) - ->findOneByTitle('Many Restrict Type'); + $this->expectException(ReferenceIntegrityStrictException::class); + $type = $this->dm->getRepository(Type::class) + ->findOneBy(['title' => 'Many Restrict Type']); - $this->assertFalse(is_null($type)); - $this->assertTrue(is_object($type)); + static::assertNotNull($type); + static::assertIsObject($type); $this->dm->remove($type); $this->dm->flush(); } - private function populateOneNullify() + private function populateOneNullify(): void { - $typeClass = self::TYPE_ONE_NULLIFY_CLASS; - $type = new $typeClass(); + $type = new TypeOneNullify(); $type->setTitle('One Nullify Type'); - $articleClass = self::ARTICLE_ONE_NULLIFY_CLASS; - $article = new $articleClass(); + $article = new ArticleOneNullify(); $article->setTitle('One Nullify Article'); $article->setType($type); @@ -206,14 +202,12 @@ private function populateOneNullify() $this->dm->clear(); } - private function populateManyNullify() + private function populateManyNullify(): void { - $typeClass = self::TYPE_MANY_NULLIFY_CLASS; - $type = new $typeClass(); + $type = new TypeManyNullify(); $type->setTitle('Many Nullify Type'); - $articleClass = self::ARTICLE_MANY_NULLIFY_CLASS; - $article = new $articleClass(); + $article = new ArticleManyNullify(); $article->setTitle('Many Nullify Article'); $article->setType($type); @@ -224,17 +218,15 @@ private function populateManyNullify() $this->dm->clear(); } - private function populateOnePull() + private function populateOnePull(): void { - $typeClass = self::TYPE_ONE_PULL_CLASS; - $type1 = new $typeClass(); + $type1 = new TypeOnePull(); $type1->setTitle('One Pull Type 1'); - $type2 = new $typeClass(); + $type2 = new TypeOnePull(); $type2->setTitle('One Pull Type 2'); - $articleClass = self::ARTICLE_ONE_PULL_CLASS; - $article = new $articleClass(); + $article = new ArticleOnePull(); $article->setTitle('One Pull Article'); $article->addType($type1); $article->addType($type2); @@ -247,17 +239,15 @@ private function populateOnePull() $this->dm->clear(); } - private function populateManyPull() + private function populateManyPull(): void { - $typeClass = self::TYPE_MANY_PULL_CLASS; - $type1 = new $typeClass(); + $type1 = new TypeManyPull(); $type1->setTitle('Many Pull Type 1'); - $type2 = new $typeClass(); + $type2 = new TypeManyPull(); $type2->setTitle('Many Pull Type 2'); - $articleClass = self::ARTICLE_MANY_PULL_CLASS; - $article = new $articleClass(); + $article = new ArticleManyPull(); $article->setTitle('Many Pull Article'); $article->addType($type1); $article->addType($type2); @@ -270,14 +260,12 @@ private function populateManyPull() $this->dm->clear(); } - private function populateOneRestrict() + private function populateOneRestrict(): void { - $typeClass = self::TYPE_ONE_RESTRICT_CLASS; - $type = new $typeClass(); + $type = new TypeOneRestrict(); $type->setTitle('One Restrict Type'); - $articleClass = self::ARTICLE_ONE_RESTRICT_CLASS; - $article = new $articleClass(); + $article = new ArticleOneRestrict(); $article->setTitle('One Restrict Article'); $article->setType($type); @@ -288,14 +276,12 @@ private function populateOneRestrict() $this->dm->clear(); } - private function populateManyRestrict() + private function populateManyRestrict(): void { - $typeClass = self::TYPE_MANY_RESTRICT_CLASS; - $type = new $typeClass(); + $type = new Type(); $type->setTitle('Many Restrict Type'); - $articleClass = self::ARTICLE_MANY_RESTRICT_CLASS; - $article = new $articleClass(); + $article = new Article(); $article->setTitle('Many Restrict Article'); $article->setType($type); diff --git a/tests/Gedmo/References/Fixture/ODM/MongoDB/Metadata.php b/tests/Gedmo/References/Fixture/ODM/MongoDB/Metadata.php index 0f2f0e38a4..bb56a7cfd1 100644 --- a/tests/Gedmo/References/Fixture/ODM/MongoDB/Metadata.php +++ b/tests/Gedmo/References/Fixture/ODM/MongoDB/Metadata.php @@ -1,50 +1,70 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\References\Fixture\ODM\MongoDB; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; -use References\Fixture\ORM\Category; +use Gedmo\Tests\References\Fixture\ORM\Category; /** * @ODM\EmbeddedDocument * Metadata of type Category */ +#[ODM\EmbeddedDocument] class Metadata { - /** @ODM\Field(type="string") */ + /** + * @var string|null + * + * @ODM\Field(type="string") + */ + #[ODM\Field(type: Type::STRING)] private $name; /** - * @Gedmo\ReferenceOne(type="entity", class="References\Fixture\ORM\Category", identifier="categoryId") + * @Gedmo\ReferenceOne(type="entity", class="Gedmo\Tests\References\Fixture\ORM\Category", identifier="categoryId") */ - private $category; + #[Gedmo\ReferenceOne(type: 'entity', class: Category::class, identifier: 'categoryId')] + private Category $category; - /** @ODM\Field(type="int") */ - private $categoryId; + /** + * @ODM\Field(type="int") + */ + #[ODM\Field(type: Type::INT)] + private ?int $categoryId = null; - public function __construct($category) + public function __construct(Category $category) { $this->setCategory($category); } - public function setCategoryId($categoryId) + public function setCategoryId(int $categoryId): void { $this->categoryId = $categoryId; } - public function getCategoryId() + public function getCategoryId(): ?int { return $this->categoryId; } - public function setCategory(Category $category) + public function setCategory(Category $category): void { $this->category = $category; $this->categoryId = $category->getId(); } - public function getCategory() + public function getCategory(): Category { return $this->category; } diff --git a/tests/Gedmo/References/Fixture/ODM/MongoDB/Product.php b/tests/Gedmo/References/Fixture/ODM/MongoDB/Product.php index 030ff84edd..56eb5bd369 100644 --- a/tests/Gedmo/References/Fixture/ODM/MongoDB/Product.php +++ b/tests/Gedmo/References/Fixture/ODM/MongoDB/Product.php @@ -1,83 +1,113 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\References\Fixture\ODM\MongoDB; + +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\Common\Collections\ArrayCollection; +use Gedmo\Tests\References\Fixture\ORM\StockItem; /** * @ODM\Document */ +#[ODM\Document] class Product { /** * @ODM\Id */ - private $id; + #[ODM\Id] + private ?string $id = null; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $name; + #[ODM\Field(type: Type::STRING)] + private ?string $name = null; /** - * @Gedmo\ReferenceMany(type="entity", class="References\Fixture\ORM\StockItem", mappedBy="product") + * @var Collection + * + * @Gedmo\ReferenceMany(type="entity", class="Gedmo\Tests\References\Fixture\ORM\StockItem", mappedBy="product") */ + #[Gedmo\ReferenceMany(type: 'entity', class: StockItem::class, mappedBy: 'product')] private $stockItems; /** - * @ODM\EmbedMany(targetDocument="References\Fixture\ODM\MongoDB\Metadata") + * @var Collection + * + * @ODM\EmbedMany(targetDocument="Gedmo\Tests\References\Fixture\ODM\MongoDB\Metadata") */ + #[ODM\EmbedMany(targetDocument: Metadata::class)] private $metadatas; public function __construct() { $this->metadatas = new ArrayCollection(); + $this->stockItems = new ArrayCollection(); } - public function getId() + public function getId(): ?string { return $this->id; } - public function setId($id) + public function setId(?string $id): void { $this->id = $id; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getStockItems() + /** + * @return Collection + */ + public function getStockItems(): Collection { return $this->stockItems; } - public function setStockItems(Collection $stockItems) + /** + * @param Collection $stockItems + */ + public function setStockItems(Collection $stockItems): void { $this->stockItems = $stockItems; } - public function addMetadata($metadata) + public function addMetadata(Metadata $metadata): void { $this->metadatas[] = $metadata; } - public function removeMetadata($metadata) + public function removeMetadata(Metadata $metadata): void { - $this->metadatas->removeElement( $metadata ); + $this->metadatas->removeElement($metadata); } - public function getMetadatas() + /** + * @return Collection + */ + public function getMetadatas(): Collection { return $this->metadatas; } diff --git a/tests/Gedmo/References/Fixture/ORM/Category.php b/tests/Gedmo/References/Fixture/ORM/Category.php index 8d60e7829c..da3e980d01 100644 --- a/tests/Gedmo/References/Fixture/ORM/Category.php +++ b/tests/Gedmo/References/Fixture/ORM/Category.php @@ -1,51 +1,81 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\References\Fixture\ORM; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; -use References\Fixture\ODM\MongoDB\Product; +use Gedmo\Tests\References\Fixture\ODM\MongoDB\Product; /** * @ORM\Entity */ +#[ORM\Entity] class Category { /** + * @var int|null + * * @ORM\Id - * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="name", type="string", length=128) */ - private $name; + #[ORM\Column(name: 'name', type: Types::STRING, length: 128)] + private ?string $name = null; /** - * @Gedmo\ReferenceManyEmbed(class="References\Fixture\ODM\MongoDB\Product", identifier="metadatas.categoryId") + * @var Collection + * + * @Gedmo\ReferenceManyEmbed(class="Gedmo\Tests\References\Fixture\ODM\MongoDB\Product", identifier="metadatas.categoryId") */ - private $products; + #[Gedmo\ReferenceManyEmbed(class: Product::class, identifier: 'metadatas.categoryId')] + private Collection $products; - public function getId() + public function __construct() + { + $this->products = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function setName($name) + public function setName(?string $name): self { $this->name = $name; return $this; } - public function getName() + public function getName(): ?string { return $this->name; } - public function getProducts() + /** + * @return Collection + */ + public function getProducts(): Collection { return $this->products; } diff --git a/tests/Gedmo/References/Fixture/ORM/StockItem.php b/tests/Gedmo/References/Fixture/ORM/StockItem.php index 192b0b0559..64e26a9cb9 100644 --- a/tests/Gedmo/References/Fixture/ORM/StockItem.php +++ b/tests/Gedmo/References/Fixture/ORM/StockItem.php @@ -1,99 +1,120 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\References\Fixture\ORM; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; -use References\Fixture\ODM\MongoDB\Product; +use Gedmo\Tests\References\Fixture\ODM\MongoDB\Product; /** * @ORM\Entity */ +#[ORM\Entity] class StockItem { /** + * @var int|null + * * @ORM\Id - * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column */ - private $name; + #[ORM\Column] + private ?string $name = null; /** * @ORM\Column */ - private $sku; + #[ORM\Column] + private ?string $sku = null; /** * @ORM\Column(type="integer") */ - private $quantity; + #[ORM\Column(type: Types::INTEGER)] + private ?int $quantity = null; /** - * @Gedmo\ReferenceOne(type="document", class="References\Fixture\ODM\MongoDB\Product", inversedBy="stockItems", identifier="productId") + * @Gedmo\ReferenceOne(type="document", class="Gedmo\Tests\References\Fixture\ODM\MongoDB\Product", inversedBy="stockItems", identifier="productId") */ - private $product; + #[Gedmo\ReferenceOne(type: 'document', class: Product::class, inversedBy: 'stockItems', identifier: 'productId')] + private ?Product $product = null; /** * @ORM\Column(type="string") */ - private $productId; + #[ORM\Column(type: Types::STRING)] + private ?string $productId = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getSku() + public function getSku(): ?string { return $this->sku; } - public function setSku($sku) + public function setSku(?string $sku): void { $this->sku = $sku; } - public function getQuantity() + public function getQuantity(): ?int { return $this->quantity; } - public function setQuantity($quantity) + public function setQuantity(?int $quantity): void { $this->quantity = $quantity; } - public function setProduct(Product $product) + public function setProduct(?Product $product): void { $this->product = $product; } - public function getProduct() + public function getProduct(): ?Product { return $this->product; } - public function setProductId($productId) + public function setProductId(?string $productId): void { $this->productId = $productId; } - public function getProductId() + public function getProductId(): ?string { return $this->productId; } diff --git a/tests/Gedmo/References/LazyCollectionTest.php b/tests/Gedmo/References/LazyCollectionTest.php new file mode 100644 index 0000000000..6305e00d5a --- /dev/null +++ b/tests/Gedmo/References/LazyCollectionTest.php @@ -0,0 +1,26 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\References; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use PHPUnit\Framework\TestCase; + +final class LazyCollectionTest extends TestCase +{ + public function testCallback(): void + { + $collection = new LazyCollection(static fn (): Collection => new ArrayCollection(['1', '2'])); + + static::assertCount(2, $collection); + } +} diff --git a/tests/Gedmo/References/ReferencesListenerTest.php b/tests/Gedmo/References/ReferencesListenerTest.php index d20cbde9bd..9b0d352225 100644 --- a/tests/Gedmo/References/ReferencesListenerTest.php +++ b/tests/Gedmo/References/ReferencesListenerTest.php @@ -1,52 +1,62 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\References; + +use Doctrine\Common\Collections\Collection; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ORM\EntityManager; +use Gedmo\References\ReferencesListener; +use Gedmo\Tests\References\Fixture\ODM\MongoDB\Metadata; +use Gedmo\Tests\References\Fixture\ODM\MongoDB\Product; +use Gedmo\Tests\References\Fixture\ORM\Category; +use Gedmo\Tests\References\Fixture\ORM\StockItem; +use Gedmo\Tests\Tool\BaseTestCaseOM; + +final class ReferencesListenerTest extends BaseTestCaseOM { - private $em; - private $dm; + private EntityManager $em; - protected function setUp() + private DocumentManager $dm; + + protected function setUp(): void { parent::setUp(); - if (!class_exists('Mongo')) { - $this->markTestSkipped('Missing Mongo extension.'); + if (!extension_loaded('mongodb')) { + static::markTestSkipped('Missing Mongo extension.'); } - $reader = new AnnotationReader(); - - $this->dm = $this->getMockDocumentManager('test', new MongoDBAnnotationDriver($reader, __DIR__.'/Fixture/ODM/MongoDB')); + $this->dm = $this->getMockDocumentManager( + 'test', + $this->getMongoDBDriver([__DIR__.'/Fixture/ODM/MongoDB']) + ); - $listener = new ReferencesListener(array( + $listener = new ReferencesListener([ 'document' => $this->dm, - )); + ]); $this->evm->addEventSubscriber($listener); - $reader = new AnnotationReader(); - - $this->em = $this->getMockSqliteEntityManager( - array( - 'References\Fixture\ORM\StockItem', - 'References\Fixture\ORM\Category', - ), - new ORMAnnotationDriver($reader, __DIR__.'/Fixture/ORM') + $this->em = $this->getDefaultMockSqliteEntityManager( + [ + StockItem::class, + Category::class, + ], + $this->getORMDriver([__DIR__.'/Fixture/ORM']) ); $listener->registerManager('entity', $this->em); } - public function testShouldPersistReferencedIdentifiersIntoIdentifierField() + public function testShouldPersistReferencedIdentifiersIntoIdentifierField(): void { $stockItem = new StockItem(); $stockItem->setName('Apple TV'); @@ -63,10 +73,10 @@ public function testShouldPersistReferencedIdentifiersIntoIdentifierField() $this->em->persist($stockItem); - $this->assertEquals($product->getId(), $stockItem->getProductId()); + static::assertSame($product->getId(), $stockItem->getProductId()); } - public function testShouldPopulateReferenceOneWithProxyFromIdentifierField() + public function testShouldPopulateReferenceOneWithProxyFromIdentifierField(): void { $product = new Product(); $product->setName('Apple TV'); @@ -86,10 +96,10 @@ public function testShouldPopulateReferenceOneWithProxyFromIdentifierField() $stockItem = $this->em->find(get_class($stockItem), $stockItem->getId()); - $this->assertSame($product, $stockItem->getProduct()); + static::assertSame($product, $stockItem->getProduct()); } - public function testShouldPopulateReferenceManyWithLazyCollectionInstance() + public function testShouldPopulateReferenceManyWithLazyCollectionInstance(): void { $product = new Product(); $product->setName('Apple TV'); @@ -117,28 +127,28 @@ public function testShouldPopulateReferenceManyWithLazyCollectionInstance() $product = $this->dm->find(get_class($product), $product->getId()); - $this->assertInstanceOf('Doctrine\Common\Collections\Collection', $product->getStockItems()); - $this->assertEquals(2, $product->getStockItems()->count()); + static::assertInstanceOf(Collection::class, $product->getStockItems()); + static::assertSame(2, $product->getStockItems()->count()); $first = $product->getStockItems()->first(); - $this->assertInstanceOf(get_class($stockItem), $first); - $this->assertEquals('APP-TV', $first->getSku()); + static::assertInstanceOf(get_class($stockItem), $first); + static::assertSame('APP-TV', $first->getSku()); $last = $product->getStockItems()->last(); - $this->assertInstanceOf(get_class($stockItem), $last); - $this->assertEquals('AMZN-APP-TV', $last->getSku()); + static::assertInstanceOf(get_class($stockItem), $last); + static::assertSame('AMZN-APP-TV', $last->getSku()); } - public function testShouldPopulateReferenceManyEmbedWithLazyCollectionInstance() + public function testShouldPopulateReferenceManyEmbedWithLazyCollectionInstance(): void { $tvCategory = new Category(); - $tvCategory->setName("Television"); + $tvCategory->setName('Television'); $this->em->persist($tvCategory); $cellPhoneCategory = new Category(); - $cellPhoneCategory->setName("CellPhone"); + $cellPhoneCategory->setName('CellPhone'); $this->em->persist($cellPhoneCategory); $this->em->clear(); @@ -152,32 +162,26 @@ public function testShouldPopulateReferenceManyEmbedWithLazyCollectionInstance() $samsungTV = new Product(); $samsungTV->setName('Samsung TV'); - $this->dm->persist( $samsungTV ); + $this->dm->persist($samsungTV); $this->dm->flush(); $iPhone = new Product(); $iPhone->setName('iPhone'); - $this->dm->persist( $iPhone ); + $this->dm->persist($iPhone); $this->dm->flush(); - $appleTV->addMetadata( $tvMetadata ); - $samsungTV->addMetadata( $tvMetadata ); - $this->dm->persist( $samsungTV ); - $this->dm->persist( $appleTV ); + $appleTV->addMetadata($tvMetadata); + $samsungTV->addMetadata($tvMetadata); + $this->dm->persist($samsungTV); + $this->dm->persist($appleTV); $this->dm->flush(); - $this->assertEquals($appleTV->getMetadatas()->first(), $tvMetadata); - $this->assertEquals($samsungTV->getMetadatas()->first(), $tvMetadata); + static::assertSame($appleTV->getMetadatas()->first()->getCategoryId(), $tvMetadata->getCategoryId()); + static::assertSame($appleTV->getMetadatas()->first()->getCategory()->getName(), $tvMetadata->getCategory()->getName()); + static::assertSame($samsungTV->getMetadatas()->first()->getCategoryId(), $tvMetadata->getCategoryId()); + static::assertSame($samsungTV->getMetadatas()->first()->getCategory()->getName(), $tvMetadata->getCategory()->getName()); $tvs = $tvCategory->getProducts(); - $this->assertNotNull($tvs); - $first = $tvs->first(); - $last = $tvs->last(); - - $this->assertInstanceOf(get_class($appleTV), $first); - $this->assertEquals('Apple TV', $first->getName()); - - $this->assertInstanceOf(get_class($samsungTV), $last); - $this->assertEquals('Samsung TV', $last->getName()); + static::assertContainsOnlyInstancesOf(Product::class, $tvs); } } diff --git a/tests/Gedmo/Sluggable/AnnotationValidationTest.php b/tests/Gedmo/Sluggable/AnnotationValidationTest.php deleted file mode 100644 index 9a596071f4..0000000000 --- a/tests/Gedmo/Sluggable/AnnotationValidationTest.php +++ /dev/null @@ -1,49 +0,0 @@ - - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class AnnotationValidationTest extends BaseTestCaseORM -{ - const TARGET = 'Sluggable\\Fixture\\Validate'; - - /** - * @test - * @expectedException Gedmo\Exception\InvalidMappingException - */ - public function shouldFailValidationOnInvalidAnnotation() - { - $evm = new EventManager(); - $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); - - $slug = new Validate(); - $slug->setTitle('My Slug'); - - $slug2 = new Validate(); - $slug2->setTitle('My Slug'); - - $this->em->persist($slug); - $this->em->persist($slug2); - $this->em->flush(); - - $this->assertEquals('my-slug', $slug2->getSlug()); - } - - protected function getUsedEntityFixtures() - { - return array( - self::TARGET, - ); - } -} diff --git a/tests/Gedmo/Sluggable/CustomTransliteratorTest.php b/tests/Gedmo/Sluggable/CustomTransliteratorTest.php index 941f10799d..eb6bf238b7 100644 --- a/tests/Gedmo/Sluggable/CustomTransliteratorTest.php +++ b/tests/Gedmo/Sluggable/CustomTransliteratorTest.php @@ -1,51 +1,66 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Article; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Article; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class CustomTransliteratorTest extends BaseTestCaseORM +final class CustomTransliteratorTest extends BaseTestCaseORM { - const ARTICLE = 'Sluggable\\Fixture\\Article'; - - public function testStandardTransliteratorFailsOnChineseCharacters() + public function testStandardTransliteratorFailsOnChineseCharacters(): void { $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); - $repo = $this->em->getRepository(self::ARTICLE); + $repo = $this->em->getRepository(Article::class); - $chinese = $repo->findOneByCode('zh'); - $this->assertEquals('bei-jing-zh', $chinese->getSlug()); + $chinese = $repo->findOneBy(['code' => 'zh']); + static::assertSame('bei-jing-zh', $chinese->getSlug()); } - public function testCanUseCustomTransliterator() + public function testCanUseCustomTransliterator(): void { $evm = new EventManager(); - $evm->addEventSubscriber(new MySluggableListener()); + $sluggableListener = new SluggableListener(); + $sluggableListener->setTransliterator([Transliterator::class, 'transliterate']); + $evm->addEventSubscriber($sluggableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); - $repo = $this->em->getRepository(self::ARTICLE); + $repo = $this->em->getRepository(Article::class); - $chinese = $repo->findOneByCode('zh'); - $this->assertEquals('bei-jing', $chinese->getSlug()); + $chinese = $repo->findOneBy(['code' => 'zh']); + static::assertSame('bei-jing', $chinese->getSlug()); } - private function populate() + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + ]; + } + + private function populate(): void { $chinese = new Article(); $chinese->setTitle('ๅŒ—ไบฌ'); @@ -54,26 +69,11 @@ private function populate() $this->em->flush(); $this->em->clear(); } - - protected function getUsedEntityFixtures() - { - return array( - self::ARTICLE, - ); - } -} - -class MySluggableListener extends SluggableListener -{ - public function __construct() - { - $this->setTransliterator(array('\Gedmo\Sluggable\Transliterator', 'transliterate')); - } } -class Transliterator +final class Transliterator { - public static function transliterate($text, $separator, $object) + public static function transliterate(string $text, string $separator, object $object): string { return 'Bei Jing'; } diff --git a/tests/Gedmo/Sluggable/Fixture/Article.php b/tests/Gedmo/Sluggable/Fixture/Article.php index a2ee974bcc..9ea26ec64a 100644 --- a/tests/Gedmo/Sluggable/Fixture/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Article.php @@ -1,66 +1,91 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; /** * @ORM\Entity */ +#[ORM\Entity] class Article implements Sluggable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @ORM\Column(name="code", type="string", length=16) */ - private $code; + #[ORM\Column(name: 'code', type: Types::STRING, length: 16)] + private ?string $code = null; /** * @Gedmo\Slug(separator="-", updatable=true, fields={"title", "code"}) + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ - private $slug; + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'code'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function setSlug($slug) + public function setSlug(?string $slug): void { $this->slug = $slug; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/ArticleWithoutFields.php b/tests/Gedmo/Sluggable/Fixture/ArticleWithoutFields.php new file mode 100644 index 0000000000..2939f64305 --- /dev/null +++ b/tests/Gedmo/Sluggable/Fixture/ArticleWithoutFields.php @@ -0,0 +1,60 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class ArticleWithoutFields implements Sluggable +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @Gedmo\Slug(separator="-", updatable=true) + * + * @ORM\Column(name="slug", type="string", length=64, unique=true) + */ + #[Gedmo\Slug(separator: '-', updatable: true)] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } + + public function getSlug(): ?string + { + return $this->slug; + } +} diff --git a/tests/Gedmo/Sluggable/Fixture/Comment.php b/tests/Gedmo/Sluggable/Fixture/Comment.php index 60c0abfb91..f58df3fec2 100644 --- a/tests/Gedmo/Sluggable/Fixture/Comment.php +++ b/tests/Gedmo/Sluggable/Fixture/Comment.php @@ -1,47 +1,65 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Comment { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(type="text") */ - private $message; + #[ORM\Column(type: Types::TEXT)] + private ?string $message = null; /** * @ORM\ManyToOne(targetEntity="TranslatableArticle", inversedBy="comments") */ - private $article; + #[ORM\ManyToOne(targetEntity: TranslatableArticle::class, inversedBy: 'comments')] + private ?TranslatableArticle $article = null; - public function setArticle(TranslatableArticle $article) + public function setArticle(TranslatableArticle $article): void { $this->article = $article; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setMessage($message) + public function setMessage(?string $message): void { $this->message = $message; } - public function getMessage() + public function getMessage(): ?string { return $this->message; } diff --git a/tests/Gedmo/Sluggable/Fixture/ConfigurationArticle.php b/tests/Gedmo/Sluggable/Fixture/ConfigurationArticle.php index c701f153be..63471b77d8 100644 --- a/tests/Gedmo/Sluggable/Fixture/ConfigurationArticle.php +++ b/tests/Gedmo/Sluggable/Fixture/ConfigurationArticle.php @@ -1,66 +1,91 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; /** * @ORM\Entity */ +#[ORM\Entity] class ConfigurationArticle implements Sluggable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @ORM\Column(name="code", type="string", length=16) */ - private $code; + #[ORM\Column(name: 'code', type: Types::STRING, length: 16)] + private ?string $code = null; /** * @Gedmo\Slug(updatable=false, unique=false, unique_base=null, fields={"title", "code"}) + * * @ORM\Column(name="slug", type="string", length=32) */ - private $slug; + #[Gedmo\Slug(updatable: false, unique: false, unique_base: null, fields: ['title', 'code'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 32)] + private ?string $slug = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function setSlug($slug) + public function setSlug(?string $slug): void { $this->slug = $slug; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDate.php b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDate.php new file mode 100644 index 0000000000..c1155b94dc --- /dev/null +++ b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDate.php @@ -0,0 +1,92 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\DateTimeTypes; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class ArticleDate implements Sluggable +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @ORM\Column(name="created_at", type="date") + */ + #[ORM\Column(name: 'created_at', type: Types::DATE_MUTABLE)] + private ?\DateTime $createdAt = null; + + /** + * @Gedmo\Slug(separator="-", updatable=true, fields={"title", "createdAt"}, dateFormat="Y-m-d") + * + * @ORM\Column(name="slug", type="string", length=64, unique=true) + */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'createdAt'], dateFormat: 'Y-m-d')] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setCreatedAt(?\DateTime $createdAt): void + { + $this->createdAt = $createdAt; + } + + public function getCreatedAt(): ?\DateTime + { + return $this->createdAt; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } + + public function getSlug(): ?string + { + return $this->slug; + } +} diff --git a/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateImmutable.php b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateImmutable.php new file mode 100644 index 0000000000..8399a60982 --- /dev/null +++ b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateImmutable.php @@ -0,0 +1,92 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\DateTimeTypes; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class ArticleDateImmutable implements Sluggable +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @ORM\Column(name="created_at", type="date_immutable") + */ + #[ORM\Column(name: 'created_at', type: Types::DATE_IMMUTABLE)] + private ?\DateTimeImmutable $createdAt = null; + + /** + * @Gedmo\Slug(separator="-", updatable=true, fields={"title", "createdAt"}, dateFormat="Y-m-d") + * + * @ORM\Column(name="slug", type="string", length=64, unique=true) + */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'createdAt'], dateFormat: 'Y-m-d')] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setCreatedAt(?\DateTimeImmutable $createdAt): void + { + $this->createdAt = $createdAt; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } + + public function getSlug(): ?string + { + return $this->slug; + } +} diff --git a/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTime.php b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTime.php new file mode 100644 index 0000000000..59eff9d7c3 --- /dev/null +++ b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTime.php @@ -0,0 +1,92 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\DateTimeTypes; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class ArticleDateTime implements Sluggable +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @ORM\Column(name="created_at", type="datetime") + */ + #[ORM\Column(name: 'created_at', type: Types::DATETIME_MUTABLE)] + private ?\DateTime $createdAt = null; + + /** + * @Gedmo\Slug(separator="-", updatable=true, fields={"title", "createdAt"}, dateFormat="Y-m-d") + * + * @ORM\Column(name="slug", type="string", length=64, unique=true) + */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'createdAt'], dateFormat: 'Y-m-d')] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setCreatedAt(?\DateTime $createdAt): void + { + $this->createdAt = $createdAt; + } + + public function getCreatedAt(): ?\DateTime + { + return $this->createdAt; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } + + public function getSlug(): ?string + { + return $this->slug; + } +} diff --git a/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTimeImmutable.php b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTimeImmutable.php new file mode 100644 index 0000000000..732d626b0c --- /dev/null +++ b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTimeImmutable.php @@ -0,0 +1,92 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\DateTimeTypes; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class ArticleDateTimeImmutable implements Sluggable +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @ORM\Column(name="created_at", type="datetime_immutable") + */ + #[ORM\Column(name: 'created_at', type: Types::DATETIME_IMMUTABLE)] + private ?\DateTimeImmutable $createdAt = null; + + /** + * @Gedmo\Slug(separator="-", updatable=true, fields={"title", "createdAt"}, dateFormat="Y-m-d") + * + * @ORM\Column(name="slug", type="string", length=64, unique=true) + */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'createdAt'], dateFormat: 'Y-m-d')] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setCreatedAt(?\DateTimeImmutable $createdAt): void + { + $this->createdAt = $createdAt; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } + + public function getSlug(): ?string + { + return $this->slug; + } +} diff --git a/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTimeTz.php b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTimeTz.php new file mode 100644 index 0000000000..db5ce39860 --- /dev/null +++ b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTimeTz.php @@ -0,0 +1,92 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\DateTimeTypes; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class ArticleDateTimeTz implements Sluggable +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @ORM\Column(name="created_at", type="datetimetz") + */ + #[ORM\Column(name: 'created_at', type: Types::DATETIMETZ_MUTABLE)] + private ?\DateTime $createdAt = null; + + /** + * @Gedmo\Slug(separator="-", updatable=true, fields={"title", "createdAt"}, dateFormat="Y-m-d") + * + * @ORM\Column(name="slug", type="string", length=64, unique=true) + */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'createdAt'], dateFormat: 'Y-m-d')] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setCreatedAt(?\DateTime $createdAt): void + { + $this->createdAt = $createdAt; + } + + public function getCreatedAt(): ?\DateTime + { + return $this->createdAt; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } + + public function getSlug(): ?string + { + return $this->slug; + } +} diff --git a/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTimeTzImmutable.php b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTimeTzImmutable.php new file mode 100644 index 0000000000..c6db077b74 --- /dev/null +++ b/tests/Gedmo/Sluggable/Fixture/DateTimeTypes/ArticleDateTimeTzImmutable.php @@ -0,0 +1,92 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\DateTimeTypes; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class ArticleDateTimeTzImmutable implements Sluggable +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @ORM\Column(name="created_at", type="datetimetz_immutable") + */ + #[ORM\Column(name: 'created_at', type: Types::DATETIMETZ_IMMUTABLE)] + private ?\DateTimeImmutable $createdAt = null; + + /** + * @Gedmo\Slug(separator="-", updatable=true, fields={"title", "createdAt"}, dateFormat="Y-m-d") + * + * @ORM\Column(name="slug", type="string", length=64, unique=true) + */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'createdAt'], dateFormat: 'Y-m-d')] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setCreatedAt(?\DateTimeImmutable $createdAt): void + { + $this->createdAt = $createdAt; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } + + public function getSlug(): ?string + { + return $this->slug; + } +} diff --git a/tests/Gedmo/Sluggable/Fixture/Doctrine/FakeFilter.php b/tests/Gedmo/Sluggable/Fixture/Doctrine/FakeFilter.php index e8df03c9a4..386042340e 100644 --- a/tests/Gedmo/Sluggable/Fixture/Doctrine/FakeFilter.php +++ b/tests/Gedmo/Sluggable/Fixture/Doctrine/FakeFilter.php @@ -1,14 +1,23 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Doctrine; + +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Filter\SQLFilter; class FakeFilter extends SQLFilter { - public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string { - // do nothing, it's a fake ! + throw new \BadMethodCallException('Do nothing, it\'s a fake !'); } } diff --git a/tests/Gedmo/Sluggable/Fixture/Document/Article.php b/tests/Gedmo/Sluggable/Fixture/Document/Article.php index 5d35f99885..90a332ecca 100644 --- a/tests/Gedmo/Sluggable/Fixture/Document/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Document/Article.php @@ -1,60 +1,83 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { - /** @ODM\Id */ + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: Type::STRING)] + private ?string $title = null; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $code; + #[ODM\Field(type: Type::STRING)] + private ?string $code = null; /** + * @var string|null + * * @Gedmo\Slug(separator="-", updatable=true, fields={"title", "code"}) - * @ODM\String + * + * @ODM\Field(type="string") */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'code'])] + #[ODM\Field(type: Type::STRING)] private $slug; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Document/Handler/Article.php b/tests/Gedmo/Sluggable/Fixture/Document/Handler/Article.php index 7665001541..15f0f9c9d4 100644 --- a/tests/Gedmo/Sluggable/Fixture/Document/Handler/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Document/Handler/Article.php @@ -1,66 +1,91 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Document\Handler; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\InversedRelativeSlugHandler; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { - /** @ODM\Id */ + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: Type::STRING)] + private ?string $title = null; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $code; + #[ODM\Field(type: Type::STRING)] + private ?string $code = null; /** + * @var string|null + * * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="relationClass", value="Sluggable\Fixture\Document\Handler\RelativeSlug"), - * @Gedmo\SlugHandlerOption(name="mappedBy", value="article"), - * @Gedmo\SlugHandlerOption(name="inverseSlugField", value="alias") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="relationClass", value="Gedmo\Tests\Sluggable\Fixture\Document\Handler\RelativeSlug"), + * @Gedmo\SlugHandlerOption(name="mappedBy", value="article"), + * @Gedmo\SlugHandlerOption(name="inverseSlugField", value="alias") + * }) * }, separator="-", updatable=true, fields={"title", "code"}) - * @ODM\String + * + * @ODM\Field(type="string") */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'code'])] + #[Gedmo\SlugHandler(class: InversedRelativeSlugHandler::class, options: ['relationClass' => RelativeSlug::class, 'mappedBy' => 'article', 'inverseSlugField' => 'alias'])] + #[ODM\Field(type: Type::STRING)] private $slug; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Document/Handler/RelativeSlug.php b/tests/Gedmo/Sluggable/Fixture/Document/Handler/RelativeSlug.php index f58750240d..042ac30b36 100644 --- a/tests/Gedmo/Sluggable/Fixture/Document/Handler/RelativeSlug.php +++ b/tests/Gedmo/Sluggable/Fixture/Document/Handler/RelativeSlug.php @@ -1,68 +1,91 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Document\Handler; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\RelativeSlugHandler; /** * @ODM\Document */ +#[ODM\Document] class RelativeSlug { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: Type::STRING)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="relationField", value="article"), - * @Gedmo\SlugHandlerOption(name="relationSlugField", value="slug"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="relationField", value="article"), + * @Gedmo\SlugHandlerOption(name="relationSlugField", value="slug"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }) * }, separator="-", updatable=true, fields={"title"}) - * @ODM\String + * + * @ODM\Field(type="string") */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title'])] + #[Gedmo\SlugHandler(class: RelativeSlugHandler::class, options: ['relationField' => 'article', 'relationSlugField' => 'slug', 'separator' => '/'])] + #[ODM\Field(type: Type::STRING)] private $alias; /** - * @ODM\ReferenceOne(targetDocument="Article") + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\Sluggable\Fixture\Document\Handler\Article") */ - private $article; + #[ODM\ReferenceOne(targetDocument: Article::class)] + private ?Article $article = null; - public function setArticle(Article $article = null) + public function setArticle(?Article $article = null): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?Article { return $this->article; } - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->alias; } diff --git a/tests/Gedmo/Sluggable/Fixture/Document/Handler/TreeSlug.php b/tests/Gedmo/Sluggable/Fixture/Document/Handler/TreeSlug.php index 7b1b1eb3a1..c9a4545138 100644 --- a/tests/Gedmo/Sluggable/Fixture/Document/Handler/TreeSlug.php +++ b/tests/Gedmo/Sluggable/Fixture/Document/Handler/TreeSlug.php @@ -1,67 +1,90 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Document\Handler; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\TreeSlugHandler; /** * @ODM\Document */ +#[ODM\Document] class TreeSlug { /** + * @var string|null + * * @ODM\Id */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: Type::STRING)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }) * }, separator="-", updatable=true, fields={"title"}) - * @ODM\String + * + * @ODM\Field(type="string") */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title'])] + #[Gedmo\SlugHandler(class: TreeSlugHandler::class, options: ['parentRelationField' => 'parent', 'separator' => '/'])] + #[ODM\Field(type: Type::STRING)] private $alias; /** - * @ODM\ReferenceOne(targetDocument="TreeSlug") + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\Sluggable\Fixture\Document\Handler\TreeSlug") */ - private $parent; + #[ODM\ReferenceOne(targetDocument: self::class)] + private ?TreeSlug $parent = null; - public function setParent(TreeSlug $parent = null) + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->alias; } diff --git a/tests/Gedmo/Sluggable/Fixture/Embeddable/Address.php b/tests/Gedmo/Sluggable/Fixture/Embeddable/Address.php new file mode 100644 index 0000000000..2c6a9593af --- /dev/null +++ b/tests/Gedmo/Sluggable/Fixture/Embeddable/Address.php @@ -0,0 +1,86 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Embeddable; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Embeddable + */ +#[ORM\Embeddable] +class Address +{ + /** + * @ORM\Column(name="street", type="string", length=64) + */ + #[ORM\Column(name: 'street', type: Types::STRING, length: 64)] + private ?string $street = null; + + /** + * @ORM\Column(name="postalCode", type="string", length=64) + */ + #[ORM\Column(name: 'postalCode', type: Types::STRING, length: 64)] + private ?string $postalCode = null; + + /** + * @ORM\Column(name="city", type="string", length=64) + */ + #[ORM\Column(name: 'city', type: Types::STRING, length: 64)] + private ?string $city = null; + + /** + * @ORM\Column(name="country", type="string", length=64) + */ + #[ORM\Column(name: 'country', type: Types::STRING, length: 64)] + private ?string $country = null; + + public function getStreet(): ?string + { + return $this->street; + } + + public function setStreet(?string $street): void + { + $this->street = $street; + } + + public function getPostalCode(): ?string + { + return $this->postalCode; + } + + public function setPostalCode(?string $postalCode): void + { + $this->postalCode = $postalCode; + } + + public function getCity(): ?string + { + return $this->city; + } + + public function setCity(?string $city): void + { + $this->city = $city; + } + + public function getCountry(): ?string + { + return $this->country; + } + + public function setCountry(?string $country): void + { + $this->country = $country; + } +} diff --git a/tests/Gedmo/Sluggable/Fixture/Embeddable/User.php b/tests/Gedmo/Sluggable/Fixture/Embeddable/User.php new file mode 100644 index 0000000000..aa90445525 --- /dev/null +++ b/tests/Gedmo/Sluggable/Fixture/Embeddable/User.php @@ -0,0 +1,96 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Embeddable; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Doctrine\ORM\Mapping\Embedded; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class User implements Sluggable +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; + + /** + * @ORM\Column(name="username", type="string", length=64) + */ + #[ORM\Column(name: 'username', type: Types::STRING, length: 64)] + private ?string $username = null; + + /** + * @Gedmo\Slug(separator="-", updatable=true, fields={"username", "address.city", "address.country"}) + * + * @ORM\Column(name="slug", type="string", length=64, unique=true) + */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['username', 'address.city', 'address.country'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; + + /** + * @ORM\Embedded(class=Address::class) + */ + #[Embedded(class: Address::class)] + private Address $address; + + public function __construct() + { + $this->address = new Address(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(?string $username): void + { + $this->username = $username; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } + + public function getSlug(): ?string + { + return $this->slug; + } + + public function getAddress(): Address + { + return $this->address; + } + + public function setAddress(Address $address): void + { + $this->address = $address; + } +} diff --git a/tests/Gedmo/Sluggable/Fixture/Handler/Article.php b/tests/Gedmo/Sluggable/Fixture/Handler/Article.php index 660d404d42..3c0770541e 100644 --- a/tests/Gedmo/Sluggable/Fixture/Handler/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Handler/Article.php @@ -1,67 +1,96 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Handler; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\InversedRelativeSlugHandler; +use Gedmo\Sluggable\Sluggable; /** * @ORM\Entity */ +#[ORM\Entity] class Article implements Sluggable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @ORM\Column(name="code", type="string", length=16) */ - private $code; + #[ORM\Column(name: 'code', type: Types::STRING, length: 16)] + private ?string $code = null; /** + * @var string|null + * * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="relationClass", value="Sluggable\Fixture\Handler\ArticleRelativeSlug"), - * @Gedmo\SlugHandlerOption(name="mappedBy", value="article"), - * @Gedmo\SlugHandlerOption(name="inverseSlugField", value="slug") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="relationClass", value="Gedmo\Tests\Sluggable\Fixture\Handler\ArticleRelativeSlug"), + * @Gedmo\SlugHandlerOption(name="mappedBy", value="article"), + * @Gedmo\SlugHandlerOption(name="inverseSlugField", value="slug") + * }) * }, separator="-", updatable=true, fields={"title", "code"}) + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'code'])] + #[Gedmo\SlugHandler(class: InversedRelativeSlugHandler::class, options: ['relationClass' => ArticleRelativeSlug::class, 'mappedBy' => 'article', 'inverseSlugField' => 'slug'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] private $slug; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Handler/ArticleRelativeSlug.php b/tests/Gedmo/Sluggable/Fixture/Handler/ArticleRelativeSlug.php index e88dc50abd..dda0412dec 100644 --- a/tests/Gedmo/Sluggable/Fixture/Handler/ArticleRelativeSlug.php +++ b/tests/Gedmo/Sluggable/Fixture/Handler/ArticleRelativeSlug.php @@ -1,70 +1,95 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Handler; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\RelativeSlugHandler; /** * @ORM\Entity */ +#[ORM\Entity] class ArticleRelativeSlug { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=64) */ - private $title; + #[ORM\Column(length: 64)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="relationField", value="article"), - * @Gedmo\SlugHandlerOption(name="relationSlugField", value="slug"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="relationField", value="article"), + * @Gedmo\SlugHandlerOption(name="relationSlugField", value="slug"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }) * }, separator="-", updatable=true, fields={"title"}) + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title'])] + #[Gedmo\SlugHandler(class: RelativeSlugHandler::class, options: ['relationField' => 'article', 'relationSlugField' => 'slug', 'separator' => '/'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] private $slug; /** * @ORM\ManyToOne(targetEntity="Article") */ - private $article; + #[ORM\ManyToOne(targetEntity: Article::class)] + private ?Article $article = null; - public function setArticle(Article $article = null) + public function setArticle(?Article $article = null): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?Article { return $this->article; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Handler/Company.php b/tests/Gedmo/Sluggable/Fixture/Handler/Company.php index 182eae47ac..2070381905 100644 --- a/tests/Gedmo/Sluggable/Fixture/Handler/Company.php +++ b/tests/Gedmo/Sluggable/Fixture/Handler/Company.php @@ -1,55 +1,79 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Handler; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\InversedRelativeSlugHandler; /** * @ORM\Entity */ +#[ORM\Entity] class Company { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=64) */ - private $title; + #[ORM\Column(length: 64)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="relationClass", value="Sluggable\Fixture\Handler\User"), - * @Gedmo\SlugHandlerOption(name="mappedBy", value="company"), - * @Gedmo\SlugHandlerOption(name="inverseSlugField", value="slug") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="relationClass", value="Gedmo\Tests\Sluggable\Fixture\Handler\User"), + * @Gedmo\SlugHandlerOption(name="mappedBy", value="company"), + * @Gedmo\SlugHandlerOption(name="inverseSlugField", value="slug") + * }) * }, fields={"title"}) + * * @ORM\Column(length=64, unique=true) */ + #[Gedmo\Slug(fields: ['title'])] + #[Gedmo\SlugHandler(class: InversedRelativeSlugHandler::class, options: ['relationClass' => User::class, 'mappedBy' => 'company', 'inverseSlugField' => 'slug'])] + #[ORM\Column(length: 64, unique: true)] private $alias; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getAlias() + public function getAlias(): ?string { return $this->alias; } diff --git a/tests/Gedmo/Sluggable/Fixture/Handler/People/Occupation.php b/tests/Gedmo/Sluggable/Fixture/Handler/People/Occupation.php index 717828030e..8707e7e4ee 100644 --- a/tests/Gedmo/Sluggable/Fixture/Handler/People/Occupation.php +++ b/tests/Gedmo/Sluggable/Fixture/Handler/People/Occupation.php @@ -1,14 +1,32 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Handler\People; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\InversedRelativeSlugHandler; +use Gedmo\Sluggable\Handler\TreeSlugHandler; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @Gedmo\Tree(type="nested") + * * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[Gedmo\Tree(type: 'nested')] class Occupation { /** @@ -16,111 +34,149 @@ class Occupation * @ORM\GeneratedValue * @ORM\Column(type="integer") */ - private $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; /** * @ORM\Column(length=64) */ - private $title; + #[ORM\Column(length: 64)] + private ?string $title = null; /** * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }), - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="relationClass", value="Sluggable\Fixture\Handler\People\Person"), - * @Gedmo\SlugHandlerOption(name="mappedBy", value="occupation"), - * @Gedmo\SlugHandlerOption(name="inverseSlugField", value="slug") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }), + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="relationClass", value="Gedmo\Tests\Sluggable\Fixture\Handler\People\Person"), + * @Gedmo\SlugHandlerOption(name="mappedBy", value="occupation"), + * @Gedmo\SlugHandlerOption(name="inverseSlugField", value="slug") + * }) * }, fields={"title"}) + * * @ORM\Column(length=64, unique=true) */ - private $slug; + #[Gedmo\Slug(fields: ['title'])] + #[Gedmo\SlugHandler(class: TreeSlugHandler::class, options: ['parentRelationField' => 'parent', 'separator' => '/'])] + #[Gedmo\SlugHandler(class: InversedRelativeSlugHandler::class, options: ['relationClass' => Person::class, 'mappedBy' => 'occupation', 'inverseSlugField' => 'slug'])] + #[ORM\Column(length: 64, unique: true)] + private ?string $slug = null; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="Occupation") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class)] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?Occupation $parent = null; + + /** + * @var Collection + */ + private Collection $children; /** * @Gedmo\TreeLeft + * * @ORM\Column(type="integer") */ - private $lft; + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private ?int $lft = null; /** * @Gedmo\TreeRight + * * @ORM\Column(type="integer") */ - private $rgt; + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeRight] + private ?int $rgt = null; /** * @Gedmo\TreeRoot + * * @ORM\Column(type="integer") */ - private $root; + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeRoot] + private ?int $root = null; -/** + /** * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer") */ - private $level; + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private ?int $level = null; - public function setParent(Occupation $parent = null) + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getChildren() + /** + * @return Collection + */ + public function getChildren(): Collection { return $this->children; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function getRoot() + public function getRoot(): ?int { return $this->root; } - public function getLeft() + public function getLeft(): ?int { return $this->lft; } - public function getRight() + public function getRight(): ?int { return $this->rgt; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Handler/People/Person.php b/tests/Gedmo/Sluggable/Fixture/Handler/People/Person.php index 3f91f670ed..2920aacd39 100644 --- a/tests/Gedmo/Sluggable/Fixture/Handler/People/Person.php +++ b/tests/Gedmo/Sluggable/Fixture/Handler/People/Person.php @@ -1,13 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Handler\People; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\RelativeSlugHandler; /** * @ORM\Entity */ +#[ORM\Entity] class Person { /** @@ -15,56 +27,65 @@ class Person * @ORM\GeneratedValue * @ORM\Column(type="integer") */ - private $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; /** * @ORM\Column(length=64) */ - private $name; + #[ORM\Column(length: 64)] + private ?string $name = null; /** * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="relationField", value="occupation"), - * @Gedmo\SlugHandlerOption(name="relationSlugField", value="slug"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="relationField", value="occupation"), + * @Gedmo\SlugHandlerOption(name="relationSlugField", value="slug"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }) * }, separator="-", updatable=true, fields={"name"}) + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ - private $slug; + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['name'])] + #[Gedmo\SlugHandler(class: RelativeSlugHandler::class, options: ['relationField' => 'occupation', 'relationSlugField' => 'slug', 'separator' => '/'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; /** * @ORM\ManyToOne(targetEntity="Occupation") */ - private $occupation; + #[ORM\ManyToOne(targetEntity: Occupation::class)] + private ?Occupation $occupation = null; - public function setOccupation(Occupation $occupation = null) + public function setOccupation(?Occupation $occupation = null): void { $this->occupation = $occupation; } - public function getOccupation() + public function getOccupation(): ?Occupation { return $this->occupation; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Handler/TreeSlug.php b/tests/Gedmo/Sluggable/Fixture/Handler/TreeSlug.php index b0d8c08645..425f2bb8af 100644 --- a/tests/Gedmo/Sluggable/Fixture/Handler/TreeSlug.php +++ b/tests/Gedmo/Sluggable/Fixture/Handler/TreeSlug.php @@ -1,122 +1,201 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Handler; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\TreeSlugHandler; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; +use Gedmo\Tree\Node; /** * @Gedmo\Tree(type="nested") + * * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") */ -class TreeSlug +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[Gedmo\Tree(type: 'nested')] +class TreeSlug implements Node { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(fields={"title"}, handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }) * }, separator="-", updatable=true) + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ + #[Gedmo\Slug(fields: ['title'], separator: '-', updatable: true)] + #[Gedmo\SlugHandler(class: TreeSlugHandler::class, options: ['parentRelationField' => 'parent', 'separator' => '/'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] private $slug; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="TreeSlug") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class)] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?TreeSlug $parent = null; /** + * @var Collection + */ + private Collection $children; + + /** + * @var int|null + * * @Gedmo\TreeLeft + * * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeLeft] private $lft; /** + * @var int|null + * * @Gedmo\TreeRight + * * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeRight] private $rgt; /** + * @var int|null + * * @Gedmo\TreeRoot + * * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeRoot] private $root; /** + * @var int|null + * * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer") */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] private $level; - public function setParent(TreeSlug $parent = null) + private ?Node $sibling = null; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getChildren() + /** + * @return Collection + */ + public function getChildren(): Collection { return $this->children; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function getRoot() + public function getRoot(): ?int { return $this->root; } - public function getLeft() + public function getLeft(): ?int { return $this->lft; } - public function getRight() + public function getRight(): ?int { return $this->rgt; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } + + public function setSibling(Node $node): void + { + $this->sibling = $node; + } + + public function getSibling(): ?Node + { + return $this->sibling; + } } diff --git a/tests/Gedmo/Sluggable/Fixture/Handler/TreeSlugPrefixSuffix.php b/tests/Gedmo/Sluggable/Fixture/Handler/TreeSlugPrefixSuffix.php index f5967a099f..87c5c821d7 100644 --- a/tests/Gedmo/Sluggable/Fixture/Handler/TreeSlugPrefixSuffix.php +++ b/tests/Gedmo/Sluggable/Fixture/Handler/TreeSlugPrefixSuffix.php @@ -1,123 +1,189 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Handler; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\TreeSlugHandler; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @Gedmo\Tree(type="nested") + * * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[Gedmo\Tree(type: 'nested')] class TreeSlugPrefixSuffix { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(fields={"title"}, handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), - * @Gedmo\SlugHandlerOption(name="separator", value="/"), - * @Gedmo\SlugHandlerOption(name="prefix", value="prefix."), - * @Gedmo\SlugHandlerOption(name="suffix", value=".suffix") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), + * @Gedmo\SlugHandlerOption(name="separator", value="/"), + * @Gedmo\SlugHandlerOption(name="prefix", value="prefix."), + * @Gedmo\SlugHandlerOption(name="suffix", value=".suffix") + * }) * }, separator="-", updatable=true) + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ + #[Gedmo\Slug(fields: ['title'], separator: '-', updatable: true)] + #[Gedmo\SlugHandler(class: TreeSlugHandler::class, options: ['parentRelationField' => 'parent', 'separator' => '/', 'prefix' => 'prefix.', 'suffix' => '.suffix'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] private $slug; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="TreeSlugPrefixSuffix") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class)] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?TreeSlugPrefixSuffix $parent = null; /** + * @var Collection + */ + private Collection $children; + + /** + * @var int|null + * * @Gedmo\TreeLeft + * * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeLeft] private $lft; /** + * @var int|null + * * @Gedmo\TreeRight + * * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeRight] private $rgt; /** + * @var int|null + * * @Gedmo\TreeRoot + * * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeRoot] private $root; /** + * @var int|null + * * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer") */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] private $level; - public function setParent(TreeSlugPrefixSuffix $parent = null) + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getChildren() + /** + * @return Collection + */ + public function getChildren(): Collection { return $this->children; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function getRoot() + public function getRoot(): ?int { return $this->root; } - public function getLeft() + public function getLeft(): ?int { return $this->lft; } - public function getRight() + public function getRight(): ?int { return $this->rgt; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Handler/User.php b/tests/Gedmo/Sluggable/Fixture/Handler/User.php index 2d7b6d9837..3096c4463f 100644 --- a/tests/Gedmo/Sluggable/Fixture/Handler/User.php +++ b/tests/Gedmo/Sluggable/Fixture/Handler/User.php @@ -1,70 +1,95 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Handler; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\RelativeSlugHandler; /** * @ORM\Entity */ +#[ORM\Entity] class User { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=64) */ - private $username; + #[ORM\Column(length: 64)] + private ?string $username = null; /** + * @var string|null + * * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="relationField", value="company"), - * @Gedmo\SlugHandlerOption(name="relationSlugField", value="alias"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="relationField", value="company"), + * @Gedmo\SlugHandlerOption(name="relationSlugField", value="alias"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }) * }, separator="-", updatable=true, fields={"username"}) + * * @ORM\Column(length=64, unique=true) */ + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['username'])] + #[Gedmo\SlugHandler(class: RelativeSlugHandler::class, options: ['relationField' => 'company', 'relationSlugField' => 'alias', 'separator' => '/'])] + #[ORM\Column(length: 64, unique: true)] private $slug; /** * @ORM\ManyToOne(targetEntity="Company") */ - private $company; + #[ORM\ManyToOne(targetEntity: Company::class)] + private ?Company $company = null; - public function setCompany(Company $company = null) + public function setCompany(?Company $company = null): void { $this->company = $company; } - public function getCompany() + public function getCompany(): ?Company { return $this->company; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setUsername($username) + public function setUsername(?string $username): void { $this->username = $username; } - public function getUsername() + public function getUsername(): ?string { return $this->username; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Identifier.php b/tests/Gedmo/Sluggable/Fixture/Identifier.php index 22928df836..8f73ac821e 100644 --- a/tests/Gedmo/Sluggable/Fixture/Identifier.php +++ b/tests/Gedmo/Sluggable/Fixture/Identifier.php @@ -1,38 +1,56 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture; -use Gedmo\Mapping\Annotation as Gedmo; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Identifier { /** + * @var string|null + * * @ORM\Id + * * @Gedmo\Slug(separator="_", updatable=false, fields={"title"}) + * * @ORM\Column(length=32, unique=true) */ + #[ORM\Id] + #[ORM\Column(length: 32, unique: true)] + #[Gedmo\Slug(separator: '_', updatable: false, fields: ['title'])] private $id; /** * @ORM\Column(length=32) */ - private $title; + #[ORM\Column(length: 32)] + private ?string $title = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Sluggable/Fixture/Inheritance/Car.php b/tests/Gedmo/Sluggable/Fixture/Inheritance/Car.php index 2849f598db..5fe4d60934 100644 --- a/tests/Gedmo/Sluggable/Fixture/Inheritance/Car.php +++ b/tests/Gedmo/Sluggable/Fixture/Inheritance/Car.php @@ -1,25 +1,36 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Inheritance; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Car extends Vehicle { /** * @ORM\Column(length=128) */ - private $description; + #[ORM\Column(length: 128)] + private ?string $description = null; - public function setDescription($description) + public function setDescription(?string $description): void { $this->description = $description; } - public function getDescription() + public function getDescription(): ?string { return $this->description; } diff --git a/tests/Gedmo/Sluggable/Fixture/Inheritance/Vehicle.php b/tests/Gedmo/Sluggable/Fixture/Inheritance/Vehicle.php index 7a7602267a..1f331e44ca 100644 --- a/tests/Gedmo/Sluggable/Fixture/Inheritance/Vehicle.php +++ b/tests/Gedmo/Sluggable/Fixture/Inheritance/Vehicle.php @@ -1,7 +1,17 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Inheritance; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; @@ -10,46 +20,61 @@ * @ORM\InheritanceType("SINGLE_TABLE") * @ORM\DiscriminatorColumn(name="discriminator", type="string") * @ORM\DiscriminatorMap({ - * "vehicle" = "Vehicle", - * "car" = "Car" + * "vehicle": "Vehicle", + * "car": "Car" * }) */ +#[ORM\Entity] +#[ORM\InheritanceType('SINGLE_TABLE')] +#[ORM\DiscriminatorColumn(name: 'discriminator', type: Types::STRING)] +#[ORM\DiscriminatorMap(['vehicle' => Vehicle::class, 'car' => Car::class])] class Vehicle { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(length: 128)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(fields={"title"}) + * * @ORM\Column(length=128, unique=true) */ + #[Gedmo\Slug(fields: ['title'])] + #[ORM\Column(length: 128, unique: true)] private $slug; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Inheritance2/Car.php b/tests/Gedmo/Sluggable/Fixture/Inheritance2/Car.php index bb1efad562..7c7fcc2f9b 100644 --- a/tests/Gedmo/Sluggable/Fixture/Inheritance2/Car.php +++ b/tests/Gedmo/Sluggable/Fixture/Inheritance2/Car.php @@ -1,6 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Inheritance2; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; @@ -8,45 +17,55 @@ /** * @ORM\Entity */ +#[ORM\Entity] class Car extends Vehicle { /** - * @ORM\Column(length=128, nullable=true) + * @var string|null + * + * @ORM\Column(length=128) */ - private $description; + #[ORM\Column(length: 128)] + protected $title; /** * @ORM\Column(length=128) */ - protected $title; + #[ORM\Column(length: 128)] + private ?string $description = null; /** + * @var string|null + * * @Gedmo\Slug(fields={"title"}) + * * @ORM\Column(length=128, unique=true) */ + #[Gedmo\Slug(fields: ['title'])] + #[ORM\Column(length: 128, unique: true)] private $slug; - public function setDescription($description) + public function setDescription(?string $description): void { $this->description = $description; } - public function getDescription() + public function getDescription(): ?string { return $this->description; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Inheritance2/SportCar.php b/tests/Gedmo/Sluggable/Fixture/Inheritance2/SportCar.php index 160bd2d3c2..b77cd52dd3 100644 --- a/tests/Gedmo/Sluggable/Fixture/Inheritance2/SportCar.php +++ b/tests/Gedmo/Sluggable/Fixture/Inheritance2/SportCar.php @@ -1,12 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Inheritance2; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class SportCar extends Car { } diff --git a/tests/Gedmo/Sluggable/Fixture/Inheritance2/Vehicle.php b/tests/Gedmo/Sluggable/Fixture/Inheritance2/Vehicle.php index c5dc251ddd..46d5d056ae 100644 --- a/tests/Gedmo/Sluggable/Fixture/Inheritance2/Vehicle.php +++ b/tests/Gedmo/Sluggable/Fixture/Inheritance2/Vehicle.php @@ -1,25 +1,45 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Inheritance2; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tests\Translatable\Fixture\Sport; /** * @ORM\Entity * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="discriment", type="string") - * @ORM\DiscriminatorMap({"vehicle" = "Vehicle", "car" = "Car", "sport" = "SportCar"}) + * @ORM\DiscriminatorMap({"vehicle": "Vehicle", "car": "Car", "sport": "SportCar"}) */ +#[ORM\Entity] +#[ORM\InheritanceType('JOINED')] +#[ORM\DiscriminatorColumn(name: 'discriminent', type: Types::STRING)] +#[ORM\DiscriminatorMap(['vehicle' => Vehicle::class, 'car' => Car::class, 'sport' => Sport::class])] abstract class Vehicle { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; - public function getId() + public function getId(): ?int { return $this->id; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue100/Article.php b/tests/Gedmo/Sluggable/Fixture/Issue100/Article.php new file mode 100644 index 0000000000..f285322886 --- /dev/null +++ b/tests/Gedmo/Sluggable/Fixture/Issue100/Article.php @@ -0,0 +1,95 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue100; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; +use Gedmo\Translatable\Translatable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class Article implements Sluggable, Translatable +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @Gedmo\Translatable + * + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + #[Gedmo\Translatable] + private ?string $title = null; + + /** + * @Gedmo\Translatable + * @Gedmo\Slug(separator="-", updatable=true, fields={"title"}, unique=true, uniqueOverTranslations=true) + * + * @ORM\Column(name="slug", type="string", length=64, unique=true) + */ + #[Gedmo\Translatable] + #[Gedmo\Slug(fields: ['title'], updatable: true, unique: true, uniqueOverTranslations: true, separator: '-')] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; + + /** + * @Gedmo\Locale + * Used locale to override Translation listener`s locale + * this is not a mapped field of entity metadata, just a simple property + */ + #[Gedmo\Locale] + private ?string $locale = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } + + public function getSlug(): ?string + { + return $this->slug; + } + + public function setTranslatableLocale(?string $locale): void + { + $this->locale = $locale; + } +} diff --git a/tests/Gedmo/Sluggable/Fixture/Issue104/Bus.php b/tests/Gedmo/Sluggable/Fixture/Issue104/Bus.php index 9c4a9e1ccd..9dd995e6dc 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue104/Bus.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue104/Bus.php @@ -1,32 +1,49 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue104; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\MappedSuperclass */ +#[ORM\MappedSuperclass] class Bus { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(length: 128)] + private ?string $title = null; - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue104/Car.php b/tests/Gedmo/Sluggable/Fixture/Issue104/Car.php index 700192528e..b4f5907c27 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue104/Car.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue104/Car.php @@ -1,30 +1,44 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue104; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Car extends Vehicle { /** + * @var string|null + * * @ORM\Column(length=128) */ + #[ORM\Column(length: 128)] protected $title; /** * @ORM\Column(length=128) */ - private $description; + #[ORM\Column(length: 128)] + private ?string $description = null; - public function setDescription($description) + public function setDescription(?string $description): void { $this->description = $description; } - public function getDescription() + public function getDescription(): ?string { return $this->description; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue104/Icarus.php b/tests/Gedmo/Sluggable/Fixture/Issue104/Icarus.php index 4f994b658f..baa3c52ced 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue104/Icarus.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue104/Icarus.php @@ -1,6 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue104; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; @@ -8,30 +17,37 @@ /** * @ORM\Entity */ +#[ORM\Entity] class Icarus extends Bus { /** * @ORM\Column(length=128) */ - private $description; + #[ORM\Column(length: 128)] + private ?string $description = null; /** + * @var string|null + * * @Gedmo\Slug(fields={"title"}) + * * @ORM\Column(length=128, unique=true) */ + #[Gedmo\Slug(fields: ['title'])] + #[ORM\Column(length: 128, unique: true)] private $slug; - public function setDescription($description) + public function setDescription(?string $description): void { $this->description = $description; } - public function getDescription() + public function getDescription(): ?string { return $this->description; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue104/Vehicle.php b/tests/Gedmo/Sluggable/Fixture/Issue104/Vehicle.php index b1da2c7cc9..d1db13f333 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue104/Vehicle.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue104/Vehicle.php @@ -1,49 +1,73 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue104; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\MappedSuperclass */ +#[ORM\MappedSuperclass] class Vehicle { /** + * @var string|null + * + * @ORM\Column(length=128) + */ + #[ORM\Column(length: 128)] + protected $title; + + /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** - * @ORM\Column(length=128) - */ - protected $title; - - /** + * @var string|null + * * @Gedmo\Slug(fields={"title"}) + * * @ORM\Column(length=128, unique=true) */ + #[Gedmo\Slug(fields: ['title'])] + #[ORM\Column(length: 128, unique: true)] private $slug; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue1058/Page.php b/tests/Gedmo/Sluggable/Fixture/Issue1058/Page.php index baf5605fc9..0786a46092 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue1058/Page.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue1058/Page.php @@ -1,113 +1,96 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue1058; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Page { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ - protected $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - protected $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** - * @var User - * - * @ORM\ManyToOne(targetEntity="Sluggable\Fixture\Issue1058\User") + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Sluggable\Fixture\Issue1058\User") * @ORM\JoinColumn(nullable=false) */ - protected $user; + #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\JoinColumn(nullable: false)] + private ?User $user = null; /** * @Gedmo\Slug(separator="-", fields={"title"}, unique=true, unique_base="user") + * * @ORM\Column(name="slug", type="string", length=64) */ - protected $slug; + #[Gedmo\Slug(separator: '-', unique: true, unique_base: 'user', fields: ['title'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64)] + private ?string $slug = null; - /** - * Getter of Id - * - * @return string - */ - public function getId() + public function getId(): ?int { return $this->id; } - /** - * Setter of Slug - * - * @param string $slug - * - * @return $this - */ - public function setSlug($slug) + public function setSlug(?string $slug): self { $this->slug = $slug; return $this; } - /** - * Getter of Slug - * - * @return string - */ - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - /** - * Setter of Title - * - * @param string $title - * - * @return $this - */ - public function setTitle($title) + public function setTitle(?string $title): self { $this->title = $title; return $this; } - /** - * Getter of Title - * - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @return User - */ - public function getUser() + public function getUser(): ?User { return $this->user; } - /** - * @param User $user - * - * @return $this - */ - public function setUser(User $user) + public function setUser(User $user): self { $this->user = $user; diff --git a/tests/Gedmo/Sluggable/Fixture/Issue1058/User.php b/tests/Gedmo/Sluggable/Fixture/Issue1058/User.php index d065175cf7..c27ab7733a 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue1058/User.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue1058/User.php @@ -1,28 +1,38 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue1058; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class User { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ - protected $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; - /** - * Getter of Id - * - * @return string - */ - public function getId() + public function getId(): ?int { return $this->id; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue1151/Article.php b/tests/Gedmo/Sluggable/Fixture/Issue1151/Article.php index 97c13b7afd..7dadbbcbe1 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue1151/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue1151/Article.php @@ -1,101 +1,79 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue1151; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** - * Sluggable\Fixture\Issue1151\Article - * * @ODM\Document */ +#[ODM\Document] class Article { /** * @ODM\Id(strategy="NONE") */ - protected $id; + #[ODM\Id(strategy: 'NONE')] + private ?string $id = null; /** - * @ODM\String + * @ODM\Field(type="string") */ - protected $title; + #[ODM\Field(type: Type::STRING)] + private ?string $title = null; /** * @Gedmo\Slug(separator="-", updatable=true, fields={"title"}) - * @ODM\String - */ - protected $slug; - - /** - * Setter of Id - * - * @param string $id * - * @return static + * @ODM\Field(type="string") */ - public function setId($id) + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title'])] + #[ODM\Field(type: Type::STRING)] + private ?string $slug = null; + + public function setId(?string $id): self { $this->id = $id; return $this; } - /** - * Getter of Id - * - * @return string - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * Setter of Slug - * - * @param string $slug - * - * @return static - */ - public function setSlug($slug) + public function setSlug(?string $slug): self { $this->slug = $slug; return $this; } - /** - * Getter of Slug - * - * @return string - */ - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - /** - * Setter of Title - * - * @param string $title - * - * @return static - */ - public function setTitle($title) + public function setTitle(?string $title): self { $this->title = $title; return $this; } - /** - * Getter of Title - * - * @return string - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue116/Country.php b/tests/Gedmo/Sluggable/Fixture/Issue116/Country.php index 866c2c4ddb..b65364aa15 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue116/Country.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue116/Country.php @@ -1,30 +1,81 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue116; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * @ORM\Table(name="sta_country") + */ +#[ORM\Entity] +#[ORM\Table(name: 'sta_country')] class Country { + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; + + /** + * @var string|null + * + * @ORM\Column(type="string", length=10, nullable=true) + */ + #[ORM\Column(type: Types::STRING, length: 10, nullable: true)] private $languageCode; - private $originalName; + + /** + * @ORM\Column(type="string", length=50) + */ + #[ORM\Column(type: Types::STRING, length: 50)] + private ?string $originalName = null; + + /** + * @var string|null + * + * @ORM\Column(type="string", length=50) + * + * @Gedmo\Slug(separator="-", fields={"originalName"}) + */ + #[ORM\Column(type: Types::STRING, length: 50)] + #[Gedmo\Slug(separator: '-', fields: ['originalName'])] private $alias; - public function getId() + public function getId(): ?int { return $this->id; } - public function setOriginalName($originalName) + public function setOriginalName(?string $originalName): void { $this->originalName = $originalName; } - public function getOriginalName() + public function getOriginalName(): ?string { return $this->originalName; } - public function getAlias() + public function getAlias(): ?string { return $this->alias; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue116/Mapping/Sluggable.Fixture.Issue116.Country.dcm.yml b/tests/Gedmo/Sluggable/Fixture/Issue116/Mapping/Gedmo.Tests.Sluggable.Fixture.Issue116.Country.dcm.yml similarity index 88% rename from tests/Gedmo/Sluggable/Fixture/Issue116/Mapping/Sluggable.Fixture.Issue116.Country.dcm.yml rename to tests/Gedmo/Sluggable/Fixture/Issue116/Mapping/Gedmo.Tests.Sluggable.Fixture.Issue116.Country.dcm.yml index d15175ad29..42f72530f1 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue116/Mapping/Sluggable.Fixture.Issue116.Country.dcm.yml +++ b/tests/Gedmo/Sluggable/Fixture/Issue116/Mapping/Gedmo.Tests.Sluggable.Fixture.Issue116.Country.dcm.yml @@ -1,4 +1,4 @@ -Sluggable\Fixture\Issue116\Country: +Gedmo\Tests\Sluggable\Fixture\Issue116\Country: type: entity table: sta_country fields: @@ -28,4 +28,4 @@ Sluggable\Fixture\Issue116\Country: fixed: false nullable: false column: original_name - lifecycleCallbacks: { } + lifecycleCallbacks: {} diff --git a/tests/Gedmo/Sluggable/Fixture/Issue1177/Article.php b/tests/Gedmo/Sluggable/Fixture/Issue1177/Article.php index 2082f0654b..3cf85b95e8 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue1177/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue1177/Article.php @@ -1,51 +1,75 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue1177; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; /** * @ORM\Entity */ +#[ORM\Entity] class Article implements Sluggable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @Gedmo\Slug(separator="-", updatable=true, fields={"title"}) + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ - private $slug; + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setSlug($slug) + public function setSlug(?string $slug): void { $this->slug = $slug; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue1240/Article.php b/tests/Gedmo/Sluggable/Fixture/Issue1240/Article.php index 30088dc405..5c7f96e079 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue1240/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue1240/Article.php @@ -1,67 +1,94 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue1240; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; /** * @ORM\Entity */ +#[ORM\Entity] class Article implements Sluggable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @Gedmo\Slug(separator="+", updatable=true, fields={"title"}) + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ - private $slug; + #[Gedmo\Slug(separator: '+', updatable: true, fields: ['title'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; /** * @Gedmo\Slug(separator="+", updatable=true, fields={"title"}, style="camel") + * * @ORM\Column(name="camel_slug", type="string", length=64, unique=true) */ - private $camelSlug; + #[ORM\Column(name: 'camel_slug', type: Types::STRING, length: 64, unique: true)] + #[Gedmo\Slug(separator: '+', updatable: true, fields: ['title'], style: 'camel')] + private ?string $camelSlug = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setSlug($slug) + public function setSlug(?string $slug): void { $this->slug = $slug; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - public function getCamelSlug() + public function getCamelSlug(): ?string { return $this->camelSlug; } - public function setCamelSlug($camelSlug) + public function setCamelSlug(?string $camelSlug): void { $this->camelSlug = $camelSlug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue131/Article.php b/tests/Gedmo/Sluggable/Fixture/Issue131/Article.php index 3bdf89eba2..0503bca60e 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue131/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue131/Article.php @@ -1,49 +1,71 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue131; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=64) */ - private $title; + #[ORM\Column(length: 64)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(updatable=true, unique=true, fields={"title"}) + * * @ORM\Column(length=64, unique=true, nullable=true) */ + #[Gedmo\Slug(updatable: true, unique: true, fields: ['title'])] + #[ORM\Column(length: 64, unique: true, nullable: true)] private $slug; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue449/Article.php b/tests/Gedmo/Sluggable/Fixture/Issue449/Article.php index ee1d6ad60b..c8fb0f2508 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue449/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue449/Article.php @@ -1,82 +1,110 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue449; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; /** * @ORM\Entity + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class Article implements Sluggable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @ORM\Column(name="code", type="string", length=16) */ - private $code; + #[ORM\Column(name: 'code', type: Types::STRING, length: 16)] + private ?string $code = null; /** * @Gedmo\Slug(separator="-", updatable=true, fields={"title", "code"}) + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ - private $slug; + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title', 'code'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; /** * @ORM\Column(name="deletedAt", type="datetime", nullable=true) */ - private $deletedAt; + #[ORM\Column(name: 'deletedAt', type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function setSlug($slug) + public function setSlug(?string $slug): void { $this->slug = $slug; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(?\DateTime $deletedAt): void { $this->deletedAt = $deletedAt; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue633/Article.php b/tests/Gedmo/Sluggable/Fixture/Issue633/Article.php index 6a2fa023a0..0dbe90d1f3 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue633/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue633/Article.php @@ -1,64 +1,87 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue633; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="code", type="string", length=16) */ - private $code; + #[ORM\Column(name: 'code', type: Types::STRING, length: 16)] + private ?string $code = null; /** * @ORM\Column(name="title", length=64) */ - private $title; + #[ORM\Column(name: 'title', length: 64)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(updatable=true, unique=true, unique_base="code", fields={"title"}) + * * @ORM\Column(length=64, nullable=true) */ + #[Gedmo\Slug(updatable: true, unique: true, unique_base: 'code', fields: ['title'])] + #[ORM\Column(length: 64, nullable: true)] private $slug; - public function getId() + public function getId(): ?int { return $this->id; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue827/Article.php b/tests/Gedmo/Sluggable/Fixture/Issue827/Article.php index 50cb888d77..0accb06459 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue827/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue827/Article.php @@ -1,65 +1,89 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue827; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", length=64) */ - private $title; + #[ORM\Column(name: 'title', length: 64)] + private ?string $title = null; /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="articles") * @ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false) */ - private $category; + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'articles')] + #[ORM\JoinColumn(name: 'category_id', referencedColumnName: 'id', nullable: false)] + private ?Category $category = null; /** + * @var string|null + * * @Gedmo\Slug(updatable=true, unique=true, unique_base="category", fields={"title"}) + * * @ORM\Column(length=64, nullable=true) */ + #[Gedmo\Slug(updatable: true, unique: true, unique_base: 'category', fields: ['title'])] + #[ORM\Column(length: 64, nullable: true)] private $slug; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - public function setCategory(Category $category) + public function setCategory(Category $category): void { $this->category = $category; } - public function getCategory() + public function getCategory(): ?Category { return $this->category; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue827/Category.php b/tests/Gedmo/Sluggable/Fixture/Issue827/Category.php index 6652030f3e..a5234c4bd8 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue827/Category.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue827/Category.php @@ -1,54 +1,86 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue827; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Category { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", length=64) */ - private $title; + #[ORM\Column(name: 'title', length: 64)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(updatable=true, unique=true, fields={"title"}) + * * @ORM\Column(length=64, nullable=true) */ + #[Gedmo\Slug(updatable: true, unique: true, fields: ['title'])] + #[ORM\Column(length: 64, nullable: true)] private $slug; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="category") */ - private $articles; + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')] + private Collection $articles; + + public function __construct() + { + $this->articles = new ArrayCollection(); + } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue827/Comment.php b/tests/Gedmo/Sluggable/Fixture/Issue827/Comment.php index 6787604f93..e464bc4748 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue827/Comment.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue827/Comment.php @@ -1,68 +1,93 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue827; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Comment { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", length=64) */ - private $title; + #[ORM\Column(name: 'title', length: 64)] + private ?string $title = null; /** - * @ORM\ManyToOne(targetEntity="Post", inversedBy="Comments") + * @ORM\ManyToOne(targetEntity="Post", inversedBy="comments") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="post_title", referencedColumnName="title", nullable=false), - * @ORM\JoinColumn(name="post_slug", referencedColumnName="slug", nullable=false) + * @ORM\JoinColumn(name="post_title", referencedColumnName="title", nullable=false), + * @ORM\JoinColumn(name="post_slug", referencedColumnName="slug", nullable=false) * }) */ - private $post; + #[ORM\ManyToOne(targetEntity: Post::class, inversedBy: 'comments')] + #[ORM\JoinColumn(name: 'post_title', referencedColumnName: 'title', nullable: false)] + #[ORM\JoinColumn(name: 'post_slug', referencedColumnName: 'slug', nullable: false)] + private ?Post $post = null; /** + * @var string|null + * * @Gedmo\Slug(updatable=true, unique=true, unique_base="post", fields={"title"}) + * * @ORM\Column(length=64, nullable=true) */ + #[Gedmo\Slug(updatable: true, unique: true, unique_base: 'post', fields: ['title'])] + #[ORM\Column(length: 64, nullable: true)] private $slug; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - public function setPost(Post $post) + public function setPost(Post $post): void { $this->post = $post; } - public function getPost() + public function getPost(): ?Post { return $this->post; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue827/Post.php b/tests/Gedmo/Sluggable/Fixture/Issue827/Post.php index 3fefdd6685..d87d520d48 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue827/Post.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue827/Post.php @@ -1,44 +1,73 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue827; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Post { /** * @ORM\Id * @ORM\Column(name="title", unique=true, length=64) */ - private $title; + #[ORM\Id] + #[ORM\Column(name: 'title', unique: true, length: 64)] + private ?string $title = null; /** + * @var string|null + * * @ORM\Id + * * @Gedmo\Slug(updatable=true, unique=true, fields={"title"}) + * * @ORM\Column(length=64, nullable=true) */ + #[ORM\Id] + #[ORM\Column(length: 64, nullable: true)] + #[Gedmo\Slug(updatable: true, unique: true, fields: ['title'])] private $slug; /** - * @ORM\OneToMany(targetEntity="Comment", mappedBy="Post") + * @var Collection + * + * @ORM\OneToMany(targetEntity="Comment", mappedBy="post") */ - private $comments; + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'post')] + private Collection $comments; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue939/Article.php b/tests/Gedmo/Sluggable/Fixture/Issue939/Article.php index b015642bce..0b6afe9d43 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue939/Article.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue939/Article.php @@ -1,65 +1,89 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue939; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", length=64) */ - private $title; + #[ORM\Column(name: 'title', length: 64)] + private ?string $title = null; /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="articles") * @ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false) */ - private $category; + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'articles')] + #[ORM\JoinColumn(name: 'category_id', referencedColumnName: 'id', nullable: false)] + private ?Category $category = null; /** + * @var string|null + * * @Gedmo\Slug(updatable=true, unique=true, unique_base="category", fields={"title"}) + * * @ORM\Column(length=64, nullable=true) */ + #[Gedmo\Slug(updatable: true, unique: true, unique_base: 'category', fields: ['title'])] + #[ORM\Column(length: 64, nullable: true)] private $slug; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - public function setCategory(Category $category) + public function setCategory(Category $category): void { $this->category = $category; } - public function getCategory() + public function getCategory(): ?Category { return $this->category; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue939/Category.php b/tests/Gedmo/Sluggable/Fixture/Issue939/Category.php index c0602162e1..aa235ada52 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue939/Category.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue939/Category.php @@ -1,54 +1,86 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue939; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Category { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", length=64) */ - private $title; + #[ORM\Column(name: 'title', length: 64)] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Slug(updatable=true, unique=true, fields={"title"}) + * * @ORM\Column(length=64, nullable=true) */ + #[Gedmo\Slug(updatable: true, unique: true, fields: ['title'])] + #[ORM\Column(name: 'slug', length: 64)] private $slug; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="category") */ - private $articles; + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')] + private Collection $articles; + + public function __construct() + { + $this->articles = new ArrayCollection(); + } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Issue939/SluggableListener.php b/tests/Gedmo/Sluggable/Fixture/Issue939/SluggableListener.php index 71f49b3673..ac2b41aab2 100644 --- a/tests/Gedmo/Sluggable/Fixture/Issue939/SluggableListener.php +++ b/tests/Gedmo/Sluggable/Fixture/Issue939/SluggableListener.php @@ -1,46 +1,62 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\Issue939; use Gedmo\Sluggable\SluggableListener as BaseSluggableListener; -class SluggableListener extends BaseSluggableListener +final class SluggableListener extends BaseSluggableListener { + /** + * @var callable(string, string, object): string + */ protected $originalTransliterator; + + /** + * @var callable(string, string, object): string + */ protected $originalUrlizer; public function __construct() { + parent::__construct(); + $this->originalTransliterator = $this->getTransliterator(); $this->originalUrlizer = $this->getUrlizer(); - $this->setTransliterator(array($this, 'transliterator')); - $this->setUrlizer(array($this, 'urlizer')); + $this->setTransliterator([$this, 'transliterator']); + $this->setUrlizer([$this, 'urlizer']); } - public function transliterator($slug, $separator = '-', $object) + public function transliterator(string $slug, string $separator, object $object): string { if ($object instanceof Article) { // custom transliteration here return $slug; } - return call_user_func_array( - $this->originalTransliterator, - array($slug, $separator, $object) - ); + $originalTransliterator = $this->originalTransliterator; + + return $originalTransliterator($slug, $separator, $object); } - public function urlizer($slug, $separator = '-', $object) + public function urlizer(string $slug, string $separator, object $object): string { if ($object instanceof Article) { // custom urlization here return $slug; } - return call_user_func_array( - $this->originalUrlizer, - array($slug, $separator, $object) - ); + $originalUrlizer = $this->originalUrlizer; + + return $originalUrlizer($slug, $separator, $object); } } diff --git a/tests/Gedmo/Sluggable/Fixture/MappedSuperclass/Car.php b/tests/Gedmo/Sluggable/Fixture/MappedSuperclass/Car.php index b1afd3a9f3..1d3a5969de 100644 --- a/tests/Gedmo/Sluggable/Fixture/MappedSuperclass/Car.php +++ b/tests/Gedmo/Sluggable/Fixture/MappedSuperclass/Car.php @@ -1,32 +1,49 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\MappedSuperclass; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Car extends Vehicle { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=128) */ - private $description; + #[ORM\Column(length: 128)] + private ?string $description = null; - public function setDescription($description) + public function setDescription(?string $description): void { $this->description = $description; } - public function getDescription() + public function getDescription(): ?string { return $this->description; } diff --git a/tests/Gedmo/Sluggable/Fixture/MappedSuperclass/Vehicle.php b/tests/Gedmo/Sluggable/Fixture/MappedSuperclass/Vehicle.php index 2245facf54..78bc20772e 100644 --- a/tests/Gedmo/Sluggable/Fixture/MappedSuperclass/Vehicle.php +++ b/tests/Gedmo/Sluggable/Fixture/MappedSuperclass/Vehicle.php @@ -1,6 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture\MappedSuperclass; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; @@ -8,35 +17,47 @@ /** * @ORM\MappedSuperclass */ +#[ORM\MappedSuperclass] class Vehicle { + /** + * @var int|null + */ + private $id; + /** * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(length: 128)] + private ?string $title = null; /** - * @Gedmo\Slug(fields={"title"}) + * @var string|null + * + * @Gedmo\Slug(fields={"title"}, updatable=false) + * * @ORM\Column(length=128, unique=true) */ + #[Gedmo\Slug(updatable: false, fields: ['title'])] + #[ORM\Column(length: 128, unique: true)] private $slug; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Page.php b/tests/Gedmo/Sluggable/Fixture/Page.php index 85cca97b8a..d409f2b6ad 100644 --- a/tests/Gedmo/Sluggable/Fixture/Page.php +++ b/tests/Gedmo/Sluggable/Fixture/Page.php @@ -1,65 +1,100 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Page { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="string", length=191) */ - private $content; + #[ORM\Column(type: Types::STRING, length: 191)] + private ?string $content = null; /** + * @var string|null + * * @Gedmo\Slug(style="camel", separator="_", fields={"content"}) + * * @ORM\Column(type="string", length=128) */ + #[Gedmo\Slug(style: 'camel', separator: '_', fields: ['content'])] + #[ORM\Column(type: Types::STRING, length: 128)] private $slug; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="TranslatableArticle", mappedBy="page") */ + #[ORM\OneToMany(targetEntity: TranslatableArticle::class, mappedBy: 'page')] private $articles; - public function getId() + public function __construct() + { + $this->articles = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function addArticle(TranslatableArticle $article) + public function addArticle(TranslatableArticle $article): void { $article->setPage($this); $this->articles[] = $article; } - public function getArticles() + /** + * @return Collection + */ + public function getArticles(): Collection { return $this->articles; } - public function setContent($content) + public function setContent(?string $content): void { $this->content = $content; } - public function getContent() + public function getContent(): ?string { return $this->content; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Position.php b/tests/Gedmo/Sluggable/Fixture/Position.php index fc3abf565e..f963510070 100644 --- a/tests/Gedmo/Sluggable/Fixture/Position.php +++ b/tests/Gedmo/Sluggable/Fixture/Position.php @@ -1,45 +1,78 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Position { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** + * @var string|null + * * @ORM\Column(length=16) */ + #[ORM\Column(length: 16)] private $prop; /** + * @var string|null + * * @ORM\Column(length=64) */ + #[ORM\Column(length: 64)] private $title; /** + * @var string|null + * * @ORM\Column(length=16) */ + #[ORM\Column(length: 16)] private $code; /** + * @var string|null + * * @ORM\Column(length=16) */ + #[ORM\Column(length: 16)] private $other; /** + * @var string|null + * * @Gedmo\Slug(fields={"code", "other", "title", "prop"}) + * * @ORM\Column(length=64, unique=true) */ + #[Gedmo\Slug(fields: ['code', 'other', 'title', 'prop'])] + #[ORM\Column(length: 64, unique: true)] private $slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/Prefix.php b/tests/Gedmo/Sluggable/Fixture/Prefix.php index 4ad4aabf40..fc057739a3 100644 --- a/tests/Gedmo/Sluggable/Fixture/Prefix.php +++ b/tests/Gedmo/Sluggable/Fixture/Prefix.php @@ -1,59 +1,77 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ -namespace Sluggable\Fixture; +namespace Gedmo\Tests\Sluggable\Fixture; -use Gedmo\Sluggable\Sluggable; -use Gedmo\Mapping\Annotation as Gedmo; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; /** * @ORM\Entity + * + * @author Dirk Luijk */ +#[ORM\Entity] class Prefix implements Sluggable { /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @Gedmo\Slug(separator="-", updatable=true, fields={"title"}, prefix="test-") + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ - private $slug; + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title'], prefix: 'test-')] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setSlug($slug) + public function setSlug(?string $slug): void { $this->slug = $slug; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/PrefixWithTreeHandler.php b/tests/Gedmo/Sluggable/Fixture/PrefixWithTreeHandler.php index 57c7394b3b..5185564055 100644 --- a/tests/Gedmo/Sluggable/Fixture/PrefixWithTreeHandler.php +++ b/tests/Gedmo/Sluggable/Fixture/PrefixWithTreeHandler.php @@ -1,198 +1,195 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ -namespace Sluggable\Fixture; +namespace Gedmo\Tests\Sluggable\Fixture; -use Gedmo\Sluggable\Sluggable; -use Gedmo\Mapping\Annotation as Gedmo; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\TreeSlugHandler; +use Gedmo\Sluggable\Sluggable; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") + * * @Gedmo\Tree(type="nested") + * + * @author Dirk Luijk */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[Gedmo\Tree(type: 'nested')] class PrefixWithTreeHandler implements Sluggable { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }) * }, separator="-", updatable=true, fields={"title"}, prefix="test.") + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ - private $slug; + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title'], prefix: 'test.')] + #[Gedmo\SlugHandler(class: TreeSlugHandler::class, options: ['parentRelationField' => 'parent', 'separator' => '/'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; /** - * @var PrefixWithTreeHandler - * * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="PrefixWithTreeHandler") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class)] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?PrefixWithTreeHandler $parent = null; /** * @Gedmo\TreeLeft + * * @ORM\Column(name="lft", type="integer") */ - private $lft; + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private ?int $lft = null; /** * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer") */ - private $lvl; + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private ?int $lvl = null; /** * @Gedmo\TreeRight + * * @ORM\Column(name="rgt", type="integer") */ - private $rgt; + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] + private ?int $rgt = null; /** * @Gedmo\TreeRoot + * * @ORM\Column(name="root", type="integer", nullable=true) */ - private $root; + #[ORM\Column(name: 'root', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeRoot] + private ?int $root = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setSlug($slug) + public function setSlug(?string $slug): void { $this->slug = $slug; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - /** - * @param PrefixWithTreeHandler $parent - * - * @return $this; - */ - public function setParent(PrefixWithTreeHandler $parent) + public function setParent(self $parent): self { $this->parent = $parent; return $this; } - /** - * @return PrefixWithTreeHandler - */ - public function getParent() + public function getParent(): ?self { return $this->parent; } - /** - * @param mixed $lft - * - * @return $this; - */ - public function setLft($lft) + public function setLft(?int $lft): self { $this->lft = $lft; return $this; } - /** - * @return mixed - */ - public function getLft() + public function getLft(): ?int { return $this->lft; } - /** - * @param mixed $lvl - * - * @return $this; - */ - public function setLvl($lvl) + public function setLvl(?int $lvl): self { $this->lvl = $lvl; return $this; } - /** - * @return mixed - */ - public function getLvl() + public function getLvl(): ?int { return $this->lvl; } - /** - * @param mixed $rgt - * - * @return $this; - */ - public function setRgt($rgt) + public function setRgt(?int $rgt): self { $this->rgt = $rgt; return $this; } - /** - * @return mixed - */ - public function getRgt() + public function getRgt(): ?int { return $this->rgt; } - /** - * @param mixed $root - * - * @return $this; - */ - public function setRoot($root) + public function setRoot(?int $root): self { $this->root = $root; return $this; } - /** - * @return mixed - */ - public function getRoot() + public function getRoot(): ?int { return $this->root; } diff --git a/tests/Gedmo/Sluggable/Fixture/Suffix.php b/tests/Gedmo/Sluggable/Fixture/Suffix.php index ec1ab32140..afba9d6b3d 100644 --- a/tests/Gedmo/Sluggable/Fixture/Suffix.php +++ b/tests/Gedmo/Sluggable/Fixture/Suffix.php @@ -1,59 +1,77 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ -namespace Sluggable\Fixture; +namespace Gedmo\Tests\Sluggable\Fixture; -use Gedmo\Sluggable\Sluggable; -use Gedmo\Mapping\Annotation as Gedmo; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Sluggable; /** * @ORM\Entity + * + * @author Dirk Luijk */ +#[ORM\Entity] class Suffix implements Sluggable { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @Gedmo\Slug(separator="-", updatable=true, fields={"title"}, suffix=".test") + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ - private $slug; + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title'], suffix: '.test')] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setSlug($slug) + public function setSlug(?string $slug): void { $this->slug = $slug; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Sluggable/Fixture/SuffixWithTreeHandler.php b/tests/Gedmo/Sluggable/Fixture/SuffixWithTreeHandler.php index fdfc40c7f6..cfb310fd22 100644 --- a/tests/Gedmo/Sluggable/Fixture/SuffixWithTreeHandler.php +++ b/tests/Gedmo/Sluggable/Fixture/SuffixWithTreeHandler.php @@ -1,198 +1,195 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ -namespace Sluggable\Fixture; +namespace Gedmo\Tests\Sluggable\Fixture; -use Gedmo\Sluggable\Sluggable; -use Gedmo\Mapping\Annotation as Gedmo; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sluggable\Handler\TreeSlugHandler; +use Gedmo\Sluggable\Sluggable; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") + * * @Gedmo\Tree(type="nested") + * + * @author Dirk Luijk */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[Gedmo\Tree(type: 'nested')] class SuffixWithTreeHandler implements Sluggable { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @Gedmo\Slug(handlers={ - * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ - * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), - * @Gedmo\SlugHandlerOption(name="separator", value="/") - * }) + * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ + * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), + * @Gedmo\SlugHandlerOption(name="separator", value="/") + * }) * }, separator="-", updatable=true, fields={"title"}, suffix=".test") + * * @ORM\Column(name="slug", type="string", length=64, unique=true) */ - private $slug; + #[Gedmo\Slug(separator: '-', updatable: true, fields: ['title'], suffix: '.test')] + #[Gedmo\SlugHandler(class: TreeSlugHandler::class, options: ['parentRelationField' => 'parent', 'separator' => '/'])] + #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] + private ?string $slug = null; /** - * @var SuffixWithTreeHandler - * * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="SuffixWithTreeHandler") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class)] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?SuffixWithTreeHandler $parent = null; /** * @Gedmo\TreeLeft + * * @ORM\Column(name="lft", type="integer") */ - private $lft; + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private ?int $lft = null; /** * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer") */ - private $lvl; + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private ?int $lvl = null; /** * @Gedmo\TreeRight + * * @ORM\Column(name="rgt", type="integer") */ - private $rgt; + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] + private ?int $rgt = null; /** * @Gedmo\TreeRoot + * * @ORM\Column(name="root", type="integer", nullable=true) */ - private $root; + #[ORM\Column(name: 'root', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeRoot] + private ?int $root = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setSlug($slug) + public function setSlug(?string $slug): void { $this->slug = $slug; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - /** - * @param SuffixWithTreeHandler $parent - * - * @return $this; - */ - public function setParent(SuffixWithTreeHandler $parent) + public function setParent(self $parent): self { $this->parent = $parent; return $this; } - /** - * @return SuffixWithTreeHandler - */ - public function getParent() + public function getParent(): ?self { return $this->parent; } - /** - * @param mixed $lft - * - * @return $this; - */ - public function setLft($lft) + public function setLft(?int $lft): self { $this->lft = $lft; return $this; } - /** - * @return mixed - */ - public function getLft() + public function getLft(): ?int { return $this->lft; } - /** - * @param mixed $lvl - * - * @return $this; - */ - public function setLvl($lvl) + public function setLvl(?int $lvl): self { $this->lvl = $lvl; return $this; } - /** - * @return mixed - */ - public function getLvl() + public function getLvl(): ?int { return $this->lvl; } - /** - * @param mixed $rgt - * - * @return $this; - */ - public function setRgt($rgt) + public function setRgt(?int $rgt): self { $this->rgt = $rgt; return $this; } - /** - * @return mixed - */ - public function getRgt() + public function getRgt(): ?int { return $this->rgt; } - /** - * @param mixed $root - * - * @return $this; - */ - public function setRoot($root) + public function setRoot(?int $root): self { $this->root = $root; return $this; } - /** - * @return mixed - */ - public function getRoot() + public function getRoot(): ?int { return $this->root; } diff --git a/tests/Gedmo/Sluggable/Fixture/TransArticleManySlug.php b/tests/Gedmo/Sluggable/Fixture/TransArticleManySlug.php index fadd8c1220..559f7dfc9c 100644 --- a/tests/Gedmo/Sluggable/Fixture/TransArticleManySlug.php +++ b/tests/Gedmo/Sluggable/Fixture/TransArticleManySlug.php @@ -1,122 +1,148 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; use Gedmo\Sluggable\Sluggable; use Gedmo\Translatable\Translatable; -use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class TransArticleManySlug implements Sluggable, Translatable { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; + private ?int $page = null; + /** * @Gedmo\Translatable + * * @ORM\Column(type="string", length=64) */ - private $title; + #[ORM\Column(type: Types::STRING, length: 64)] + #[Gedmo\Translatable] + private ?string $title = null; /** * @ORM\Column(type="string", length=64) */ - private $uniqueTitle; + #[ORM\Column(type: Types::STRING, length: 64)] + private ?string $uniqueTitle = null; /** + * @var string|null + * * @Gedmo\Slug(fields={"uniqueTitle"}) + * * @ORM\Column(type="string", length=128) */ + #[Gedmo\Slug(fields: ['uniqueTitle'])] + #[ORM\Column(type: Types::STRING, length: 128)] private $uniqueSlug; /** * @Gedmo\Translatable + * * @ORM\Column(type="string", length=16) */ - private $code; + #[ORM\Column(type: Types::STRING, length: 16)] + #[Gedmo\Translatable] + private ?string $code = null; /** + * @var string|null + * * @Gedmo\Translatable * @Gedmo\Slug(fields={"title", "code"}) + * * @ORM\Column(type="string", length=128) */ + #[ORM\Column(type: Types::STRING, length: 128)] + #[Gedmo\Slug(fields: ['title', 'code'])] + #[Gedmo\Translatable] private $slug; /** * @Gedmo\Locale * Used locale to override Translation listener`s locale */ - private $locale; - - public function addComment(Comment $comment) - { - $comment->setArticle($this); - $this->comments[] = $comment; - } - - public function getComments() - { - return $this->comments; - } + #[Gedmo\Locale] + private ?string $locale = null; - public function setPage($page) + public function setPage(?int $page): void { $this->page = $page; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setUniqueTitle($uniqueTitle) + public function setUniqueTitle(?string $uniqueTitle): void { $this->uniqueTitle = $uniqueTitle; } - public function getUniqueTitle() + public function getUniqueTitle(): ?string { return $this->uniqueTitle; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - public function getUniqueSlug() + public function getUniqueSlug(): ?string { return $this->uniqueSlug; } - public function setTranslatableLocale($locale) + public function setTranslatableLocale(?string $locale): void { $this->locale = $locale; } diff --git a/tests/Gedmo/Sluggable/Fixture/TranslatableArticle.php b/tests/Gedmo/Sluggable/Fixture/TranslatableArticle.php index 6480751ec9..951a0a04fb 100644 --- a/tests/Gedmo/Sluggable/Fixture/TranslatableArticle.php +++ b/tests/Gedmo/Sluggable/Fixture/TranslatableArticle.php @@ -1,106 +1,149 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; use Gedmo\Sluggable\Sluggable; use Gedmo\Translatable\Translatable; -use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class TranslatableArticle implements Sluggable, Translatable { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(type="string", length=64) */ - private $title; + #[ORM\Column(type: Types::STRING, length: 64)] + #[Gedmo\Translatable] + private ?string $title = null; /** * @Gedmo\Translatable + * * @ORM\Column(type="string", length=16) */ - private $code; + #[ORM\Column(type: Types::STRING, length: 16)] + #[Gedmo\Translatable] + private ?string $code = null; /** + * @var string|null + * * @Gedmo\Translatable * @Gedmo\Slug(fields={"title", "code"}) + * * @ORM\Column(type="string", length=128) */ + #[ORM\Column(type: Types::STRING, length: 128)] + #[Gedmo\Translatable] + #[Gedmo\Slug(fields: ['title', 'code'])] private $slug; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Comment", mappedBy="article") */ + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article')] private $comments; /** * @ORM\ManyToOne(targetEntity="Page", inversedBy="articles") */ - private $page; + #[ORM\ManyToOne(targetEntity: Page::class, inversedBy: 'articles')] + private ?Page $page = null; /** * @Gedmo\Locale * Used locale to override Translation listener`s locale */ - private $locale; + #[Gedmo\Language] + private ?string $locale = null; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } - public function addComment(Comment $comment) + public function addComment(Comment $comment): void { $comment->setArticle($this); $this->comments[] = $comment; } - public function getComments() + /** + * @return Collection + */ + public function getComments(): Collection { return $this->comments; } - public function setPage($page) + public function setPage(?Page $page): void { $this->page = $page; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - public function setTranslatableLocale($locale) + public function setTranslatableLocale(?string $locale): void { $this->locale = $locale; } diff --git a/tests/Gedmo/Sluggable/Fixture/Validate.php b/tests/Gedmo/Sluggable/Fixture/Validate.php deleted file mode 100644 index 23ced51157..0000000000 --- a/tests/Gedmo/Sluggable/Fixture/Validate.php +++ /dev/null @@ -1,50 +0,0 @@ -id; - } - - public function setTitle($title) - { - $this->title = $title; - } - - public function getTitle() - { - return $this->title; - } - - public function getSlug() - { - return $this->slug; - } -} diff --git a/tests/Gedmo/Sluggable/Handlers/BothSlugHandlerTest.php b/tests/Gedmo/Sluggable/Handlers/BothSlugHandlerTest.php index aff033ac52..7fe2424552 100644 --- a/tests/Gedmo/Sluggable/Handlers/BothSlugHandlerTest.php +++ b/tests/Gedmo/Sluggable/Handlers/BothSlugHandlerTest.php @@ -1,26 +1,31 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Handlers; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Handler\People\Occupation; -use Sluggable\Fixture\Handler\People\Person; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Handler\People\Occupation; +use Gedmo\Tests\Sluggable\Fixture\Handler\People\Person; +use Gedmo\Tests\Tool\BaseTestCaseORM; use Gedmo\Tree\TreeListener; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class BothSlugHandlerTest extends BaseTestCaseORM +final class BothSlugHandlerTest extends BaseTestCaseORM { - const OCCUPATION = "Sluggable\\Fixture\\Handler\\People\\Occupation"; - const PERSON = "Sluggable\\Fixture\\Handler\\People\\Person"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -28,37 +33,37 @@ protected function setUp() $evm->addEventSubscriber(new TreeListener()); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testSlugGeneration() + public function testSlugGeneration(): void { $this->populate(); - $repo = $this->em->getRepository(self::PERSON); + $repo = $this->em->getRepository(Person::class); - $herzult = $repo->findOneByName('Herzult'); - $this->assertEquals('web/developer/php/herzult', $herzult->getSlug()); + $herzult = $repo->findOneBy(['name' => 'Herzult']); + static::assertSame('web/developer/php/herzult', $herzult->getSlug()); - $gedi = $repo->findOneByName('Gedi'); - $this->assertEquals('web/developer/gedi', $gedi->getSlug()); + $gedi = $repo->findOneBy(['name' => 'Gedi']); + static::assertSame('web/developer/gedi', $gedi->getSlug()); - $hurty = $repo->findOneByName('Hurty'); - $this->assertEquals('singer/hurty', $hurty->getSlug()); + $hurty = $repo->findOneBy(['name' => 'Hurty']); + static::assertSame('singer/hurty', $hurty->getSlug()); } - public function testSlugUpdates() + public function testSlugUpdates(): void { $this->populate(); - $repo = $this->em->getRepository(self::PERSON); + $repo = $this->em->getRepository(Person::class); - $gedi = $repo->findOneByName('Gedi'); + $gedi = $repo->findOneBy(['name' => 'Gedi']); $gedi->setName('Upd Gedi'); $this->em->persist($gedi); $this->em->flush(); - $this->assertEquals('web/developer/upd-gedi', $gedi->getSlug()); + static::assertSame('web/developer/upd-gedi', $gedi->getSlug()); - $artist = $this->em->getRepository(self::OCCUPATION)->findOneByTitle('Singer'); + $artist = $this->em->getRepository(Occupation::class)->findOneBy(['title' => 'Singer']); $artist->setTitle('Artist'); $this->em->persist($artist); @@ -68,51 +73,49 @@ public function testSlugUpdates() $this->em->persist($gedi); $this->em->flush(); - $this->assertEquals('artist/upd-gedi', $gedi->getSlug()); + static::assertSame('artist/upd-gedi', $gedi->getSlug()); - $hurty = $repo->findOneByName('Hurty'); - $this->assertEquals('artist/hurty', $hurty->getSlug()); + $hurty = $repo->findOneBy(['name' => 'Hurty']); + static::assertSame('artist/hurty', $hurty->getSlug()); } - public function test1093() + public function test1093(): void { $this->populate(); - $personRepo = $this->em->getRepository(self::PERSON); - $occupationRepo = $this->em->getRepository(self::OCCUPATION); + $personRepo = $this->em->getRepository(Person::class); + $occupationRepo = $this->em->getRepository(Occupation::class); - $herzult = $personRepo->findOneByName('Herzult'); - $this->assertEquals('web/developer/php/herzult', $herzult->getSlug()); + $herzult = $personRepo->findOneBy(['name' => 'Herzult']); + static::assertSame('web/developer/php/herzult', $herzult->getSlug()); - $developer = $occupationRepo->findOneByTitle('Developer'); + $developer = $occupationRepo->findOneBy(['title' => 'Developer']); $developer->setTitle('Enthusiast'); $this->em->persist($developer); $this->em->flush(); - // Works (but is not updated in the actual DB) - $herzult = $personRepo->findOneByName('Herzult'); - $this->assertEquals('web/enthusiast/php/herzult', $herzult->getSlug()); - + $herzult = $personRepo->findOneBy(['name' => 'Herzult']); + static::assertSame('web/enthusiast/php/herzult', $herzult->getSlug()); $this->em->clear(); // Does not work. - $herzult = $personRepo->findOneByName('Herzult'); - $this->assertEquals('web/enthusiast/php/herzult', $herzult->getSlug()); + $herzult = $personRepo->findOneBy(['name' => 'Herzult']); + static::assertSame('web/enthusiast/php/herzult', $herzult->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::OCCUPATION, - self::PERSON, - ); + return [ + Occupation::class, + Person::class, + ]; } - private function populate() + private function populate(): void { - $repo = $this->em->getRepository(self::OCCUPATION); + $repo = $this->em->getRepository(Occupation::class); $web = new Occupation(); $web->setTitle('Web'); diff --git a/tests/Gedmo/Sluggable/Handlers/RelativeSlugHandlerDocumentTest.php b/tests/Gedmo/Sluggable/Handlers/RelativeSlugHandlerDocumentTest.php index 09d1036d34..cb04269c5d 100644 --- a/tests/Gedmo/Sluggable/Handlers/RelativeSlugHandlerDocumentTest.php +++ b/tests/Gedmo/Sluggable/Handlers/RelativeSlugHandlerDocumentTest.php @@ -1,86 +1,91 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Handlers; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Sluggable\Fixture\Document\Handler\Article; -use Sluggable\Fixture\Document\Handler\RelativeSlug; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Document\Handler\Article; +use Gedmo\Tests\Sluggable\Fixture\Document\Handler\RelativeSlug; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class RelativeSlugHandlerDocumentTest extends BaseTestCaseMongoODM +final class RelativeSlugHandlerDocumentTest extends BaseTestCaseMongoODM { - const ARTICLE = 'Sluggable\\Fixture\\Document\\Handler\\Article'; - const SLUG = 'Sluggable\\Fixture\\Document\\Handler\\RelativeSlug'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); } - public function testSlugGeneration() + public function testSlugGeneration(): void { $this->populate(); - $repo = $this->dm->getRepository(self::SLUG); + $repo = $this->dm->getRepository(RelativeSlug::class); - $thomas = $repo->findOneByTitle('Thomas'); - $this->assertEquals('sport-test/thomas', $thomas->getSlug()); + $thomas = $repo->findOneBy(['title' => 'Thomas']); + static::assertSame('sport-test/thomas', $thomas->getSlug()); - $jen = $repo->findOneByTitle('Jen'); - $this->assertEquals('sport-test/jen', $jen->getSlug()); + $jen = $repo->findOneBy(['title' => 'Jen']); + static::assertSame('sport-test/jen', $jen->getSlug()); - $john = $repo->findOneByTitle('John'); - $this->assertEquals('cars-code/john', $john->getSlug()); + $john = $repo->findOneBy(['title' => 'John']); + static::assertSame('cars-code/john', $john->getSlug()); - $single = $repo->findOneByTitle('Single'); - $this->assertEquals('single', $single->getSlug()); + $single = $repo->findOneBy(['title' => 'Single']); + static::assertSame('single', $single->getSlug()); } - public function testUpdateOperations() + public function testUpdateOperations(): void { $this->populate(); - $repo = $this->dm->getRepository(self::SLUG); + $repo = $this->dm->getRepository(RelativeSlug::class); - $thomas = $repo->findOneByTitle('Thomas'); + $thomas = $repo->findOneBy(['title' => 'Thomas']); $thomas->setTitle('Ninja'); $this->dm->persist($thomas); $this->dm->flush(); - $this->assertEquals('sport-test/ninja', $thomas->getSlug()); + static::assertSame('sport-test/ninja', $thomas->getSlug()); - $sport = $this->dm->getRepository(self::ARTICLE)->findOneByTitle('Sport'); + $sport = $this->dm->getRepository(Article::class)->findOneBy(['title' => 'Sport']); $sport->setTitle('Martial Arts'); $this->dm->persist($sport); $this->dm->flush(); - $this->assertEquals('martial-arts-test', $sport->getSlug()); + static::assertSame('martial-arts-test', $sport->getSlug()); - $this->assertEquals('martial-arts-test/ninja', $thomas->getSlug()); + static::assertSame('martial-arts-test/ninja', $thomas->getSlug()); - $jen = $repo->findOneByTitle('Jen'); - $this->assertEquals('martial-arts-test/jen', $jen->getSlug()); + $jen = $repo->findOneBy(['title' => 'Jen']); + static::assertSame('martial-arts-test/jen', $jen->getSlug()); - $cars = $this->dm->getRepository(self::ARTICLE)->findOneByTitle('Cars'); + $cars = $this->dm->getRepository(Article::class)->findOneBy(['title' => 'Cars']); $jen->setArticle($cars); $this->dm->persist($jen); $this->dm->flush(); - $this->assertEquals('cars-code/jen', $jen->getSlug()); + static::assertSame('cars-code/jen', $jen->getSlug()); } - private function populate() + private function populate(): void { $sport = new Article(); $sport->setTitle('Sport'); diff --git a/tests/Gedmo/Sluggable/Handlers/RelativeSlugHandlerTest.php b/tests/Gedmo/Sluggable/Handlers/RelativeSlugHandlerTest.php index a287ea06fd..95478ff05d 100644 --- a/tests/Gedmo/Sluggable/Handlers/RelativeSlugHandlerTest.php +++ b/tests/Gedmo/Sluggable/Handlers/RelativeSlugHandlerTest.php @@ -1,93 +1,98 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Handlers; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Handler\Article; -use Sluggable\Fixture\Handler\ArticleRelativeSlug; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Handler\Article; +use Gedmo\Tests\Sluggable\Fixture\Handler\ArticleRelativeSlug; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class RelativeSlugHandlerTest extends BaseTestCaseORM +final class RelativeSlugHandlerTest extends BaseTestCaseORM { - const SLUG = "Sluggable\\Fixture\\Handler\\ArticleRelativeSlug"; - const ARTICLE = "Sluggable\\Fixture\\Handler\\Article"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testSlugGeneration() + public function testSlugGeneration(): void { $this->populate(); - $repo = $this->em->getRepository(self::SLUG); + $repo = $this->em->getRepository(ArticleRelativeSlug::class); - $thomas = $repo->findOneByTitle('Thomas'); - $this->assertEquals('sport-test/thomas', $thomas->getSlug()); + $thomas = $repo->findOneBy(['title' => 'Thomas']); + static::assertSame('sport-test/thomas', $thomas->getSlug()); - $jen = $repo->findOneByTitle('Jen'); - $this->assertEquals('sport-test/jen', $jen->getSlug()); + $jen = $repo->findOneBy(['title' => 'Jen']); + static::assertSame('sport-test/jen', $jen->getSlug()); - $john = $repo->findOneByTitle('John'); - $this->assertEquals('cars-code/john', $john->getSlug()); + $john = $repo->findOneBy(['title' => 'John']); + static::assertSame('cars-code/john', $john->getSlug()); - $single = $repo->findOneByTitle('Single'); - $this->assertEquals('single', $single->getSlug()); + $single = $repo->findOneBy(['title' => 'Single']); + static::assertSame('single', $single->getSlug()); } - public function testUpdateOperations() + public function testUpdateOperations(): void { $this->populate(); - $repo = $this->em->getRepository(self::SLUG); + $repo = $this->em->getRepository(ArticleRelativeSlug::class); - $thomas = $repo->findOneByTitle('Thomas'); + $thomas = $repo->findOneBy(['title' => 'Thomas']); $thomas->setTitle('Ninja'); $this->em->persist($thomas); $this->em->flush(); - $this->assertEquals('sport-test/ninja', $thomas->getSlug()); + static::assertSame('sport-test/ninja', $thomas->getSlug()); - $sport = $this->em->getRepository(self::ARTICLE)->findOneByTitle('Sport'); + $sport = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); $sport->setTitle('Martial Arts'); $this->em->persist($sport); $this->em->flush(); - $this->assertEquals('martial-arts-test/ninja', $thomas->getSlug()); + static::assertSame('martial-arts-test/ninja', $thomas->getSlug()); - $jen = $repo->findOneByTitle('Jen'); - $this->assertEquals('martial-arts-test/jen', $jen->getSlug()); + $jen = $repo->findOneBy(['title' => 'Jen']); + static::assertSame('martial-arts-test/jen', $jen->getSlug()); - $cars = $this->em->getRepository(self::ARTICLE)->findOneByTitle('Cars'); + $cars = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Cars']); $jen->setArticle($cars); $this->em->persist($jen); $this->em->flush(); - $this->assertEquals('cars-code/jen', $jen->getSlug()); + static::assertSame('cars-code/jen', $jen->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::SLUG, - self::ARTICLE, - ); + return [ + ArticleRelativeSlug::class, + Article::class, + ]; } - private function populate() + private function populate(): void { $sport = new Article(); $sport->setTitle('Sport'); diff --git a/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerDocumentTest.php b/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerDocumentTest.php index 086deb0473..65ef2f7fe8 100644 --- a/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerDocumentTest.php +++ b/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerDocumentTest.php @@ -1,23 +1,29 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Handlers; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Sluggable\Fixture\Document\Handler\TreeSlug; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Document\Handler\TreeSlug; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TreeSlugHandlerDocumentTest extends BaseTestCaseMongoODM +final class TreeSlugHandlerDocumentTest extends BaseTestCaseMongoODM { - const SLUG = 'Sluggable\\Fixture\\Document\\Handler\\TreeSlug'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); @@ -26,55 +32,55 @@ protected function setUp() $this->getMockDocumentManager($evm); } - public function testSlugGeneration() + public function testSlugGeneration(): void { $this->populate(); - $repo = $this->dm->getRepository(self::SLUG); + $repo = $this->dm->getRepository(TreeSlug::class); - $food = $repo->findOneByTitle('Food'); - $this->assertEquals('food', $food->getSlug()); + $food = $repo->findOneBy(['title' => 'Food']); + static::assertSame('food', $food->getSlug()); - $fruits = $repo->findOneByTitle('Fruits'); - $this->assertEquals('food/fruits', $fruits->getSlug()); + $fruits = $repo->findOneBy(['title' => 'Fruits']); + static::assertSame('food/fruits', $fruits->getSlug()); - $oranges = $repo->findOneByTitle('Oranges'); - $this->assertEquals('food/fruits/oranges', $oranges->getSlug()); + $oranges = $repo->findOneBy(['title' => 'Oranges']); + static::assertSame('food/fruits/oranges', $oranges->getSlug()); - $citrons = $repo->findOneByTitle('Citrons'); - $this->assertEquals('food/fruits/citrons', $citrons->getSlug()); + $citrons = $repo->findOneBy(['title' => 'Citrons']); + static::assertSame('food/fruits/citrons', $citrons->getSlug()); } - public function testSlugUpdates() + public function testSlugUpdates(): void { $this->populate(); - $repo = $this->dm->getRepository(self::SLUG); + $repo = $this->dm->getRepository(TreeSlug::class); - $fruits = $repo->findOneByTitle('Fruits'); + $fruits = $repo->findOneBy(['title' => 'Fruits']); $fruits->setTitle('Fructis'); $this->dm->persist($fruits); $this->dm->flush(); - $this->assertEquals('food/fructis', $fruits->getSlug()); + static::assertSame('food/fructis', $fruits->getSlug()); - $oranges = $repo->findOneByTitle('Oranges'); - $this->assertEquals('food/fructis/oranges', $oranges->getSlug()); + $oranges = $repo->findOneBy(['title' => 'Oranges']); + static::assertSame('food/fructis/oranges', $oranges->getSlug()); - $citrons = $repo->findOneByTitle('Citrons'); - $this->assertEquals('food/fructis/citrons', $citrons->getSlug()); + $citrons = $repo->findOneBy(['title' => 'Citrons']); + static::assertSame('food/fructis/citrons', $citrons->getSlug()); - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $food->setTitle('Foodissimo'); $this->dm->persist($food); $this->dm->flush(); - $this->assertEquals('foodissimo', $food->getSlug()); - $this->assertEquals('foodissimo/fructis/oranges', $oranges->getSlug()); - $this->assertEquals('foodissimo/fructis/citrons', $citrons->getSlug()); + static::assertSame('foodissimo', $food->getSlug()); + static::assertSame('foodissimo/fructis/oranges', $oranges->getSlug()); + static::assertSame('foodissimo/fructis/citrons', $citrons->getSlug()); } - private function populate() + private function populate(): void { $food = new TreeSlug(); $food->setTitle('Food'); diff --git a/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerPrefixSuffixTest.php b/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerPrefixSuffixTest.php index b64f82912f..3b635fe1b1 100644 --- a/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerPrefixSuffixTest.php +++ b/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerPrefixSuffixTest.php @@ -1,17 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Handlers; use Doctrine\Common\EventManager; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Handler\TreeSlugPrefixSuffix; +use Gedmo\Tests\Tool\BaseTestCaseORM; use Gedmo\Tree\TreeListener; -use Sluggable\Fixture\Handler\TreeSlugPrefixSuffix; -use Tool\BaseTestCaseORM; -class TreeSlugHandlerPrefixSuffixTest extends BaseTestCaseORM +final class TreeSlugHandlerPrefixSuffixTest extends BaseTestCaseORM { - const TARGET = "Sluggable\\Fixture\\Handler\\TreeSlugPrefixSuffix"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -19,10 +27,10 @@ protected function setUp() $evm->addEventSubscriber(new SluggableListener()); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testPrefixSuffix() + public function testPrefixSuffix(): void { $foo = new TreeSlugPrefixSuffix(); $foo->setTitle('Foo'); @@ -41,13 +49,13 @@ public function testPrefixSuffix() $this->em->flush(); - $this->assertEquals('prefix.foo/bar/baz.suffix', $baz->getSlug()); + static::assertSame('prefix.foo/bar/baz.suffix', $baz->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TARGET, - ); + return [ + TreeSlugPrefixSuffix::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerTest.php b/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerTest.php index 0841f731cc..c392378e01 100644 --- a/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerTest.php +++ b/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerTest.php @@ -1,24 +1,30 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Handlers; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Handler\TreeSlug; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Handler\TreeSlug; +use Gedmo\Tests\Tool\BaseTestCaseORM; use Gedmo\Tree\TreeListener; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TreeSlugHandlerTest extends BaseTestCaseORM +final class TreeSlugHandlerTest extends BaseTestCaseORM { - const TARGET = "Sluggable\\Fixture\\Handler\\TreeSlug"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -26,112 +32,112 @@ protected function setUp() $evm->addEventSubscriber(new SluggableListener()); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testSlugGeneration() + public function testSlugGeneration(): void { $this->populate(); - $repo = $this->em->getRepository(self::TARGET); + $repo = $this->em->getRepository(TreeSlug::class); - $food = $repo->findOneByTitle('Food'); - $this->assertEquals('food', $food->getSlug()); + $food = $repo->findOneBy(['title' => 'Food']); + static::assertSame('food', $food->getSlug()); - $fruits = $repo->findOneByTitle('Fruits'); - $this->assertEquals('food/fruits', $fruits->getSlug()); + $fruits = $repo->findOneBy(['title' => 'Fruits']); + static::assertSame('food/fruits', $fruits->getSlug()); - $oranges = $repo->findOneByTitle('Oranges'); - $this->assertEquals('food/fruits/oranges', $oranges->getSlug()); + $oranges = $repo->findOneBy(['title' => 'Oranges']); + static::assertSame('food/fruits/oranges', $oranges->getSlug()); - $citrons = $repo->findOneByTitle('Citrons'); - $this->assertEquals('food/fruits/citrons', $citrons->getSlug()); + $citrons = $repo->findOneBy(['title' => 'Citrons']); + static::assertSame('food/fruits/citrons', $citrons->getSlug()); - $apple = $repo->findOneByTitle('Apple'); - $this->assertEquals('food/fruits/apple', $apple->getSlug()); + $apple = $repo->findOneBy(['title' => 'Apple']); + static::assertSame('food/fruits/apple', $apple->getSlug()); - $kiwi = $repo->findOneByTitle('Kiwi'); - $this->assertEquals('food/fruits/kiwi', $kiwi->getSlug()); + $kiwi = $repo->findOneBy(['title' => 'Kiwi']); + static::assertSame('food/fruits/kiwi', $kiwi->getSlug()); - $banana = $repo->findOneByTitle('Banana'); - $this->assertEquals('food/fruits/banana', $banana->getSlug()); + $banana = $repo->findOneBy(['title' => 'Banana']); + static::assertSame('food/fruits/banana', $banana->getSlug()); } - public function testSlugUpdates() + public function testSlugUpdates(): void { $this->populate(); - $repo = $this->em->getRepository(self::TARGET); + $repo = $this->em->getRepository(TreeSlug::class); - $fruits = $repo->findOneByTitle('Fruits'); + $fruits = $repo->findOneBy(['title' => 'Fruits']); $fruits->setTitle('Fructis'); $this->em->persist($fruits); $this->em->flush(); - $this->assertEquals('food/fructis', $fruits->getSlug()); + static::assertSame('food/fructis', $fruits->getSlug()); - $oranges = $repo->findOneByTitle('Oranges'); - $this->assertEquals('food/fructis/oranges', $oranges->getSlug()); + $oranges = $repo->findOneBy(['title' => 'Oranges']); + static::assertSame('food/fructis/oranges', $oranges->getSlug()); - $citrons = $repo->findOneByTitle('Citrons'); - $this->assertEquals('food/fructis/citrons', $citrons->getSlug()); + $citrons = $repo->findOneBy(['title' => 'Citrons']); + static::assertSame('food/fructis/citrons', $citrons->getSlug()); - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $food->setTitle('Foodissimo'); $this->em->persist($food); $this->em->flush(); - $this->assertEquals('foodissimo', $food->getSlug()); - $this->assertEquals('foodissimo/fructis/oranges', $oranges->getSlug()); - $this->assertEquals('foodissimo/fructis/citrons', $citrons->getSlug()); + static::assertSame('foodissimo', $food->getSlug()); + static::assertSame('foodissimo/fructis/oranges', $oranges->getSlug()); + static::assertSame('foodissimo/fructis/citrons', $citrons->getSlug()); } - public function testMoreSlugUpdates() + public function testMoreSlugUpdates(): void { $this->populate(); - $repo = $this->em->getRepository(self::TARGET); + $repo = $this->em->getRepository(TreeSlug::class); - $fruits = $repo->findOneByTitle('Fruits'); + $fruits = $repo->findOneBy(['title' => 'Fruits']); $fruits->setTitle('Fructis'); - $milk = $repo->findOneByTitle('Milk'); + $milk = $repo->findOneBy(['title' => 'Milk']); $repo->persistAsFirstChildOf($fruits, $milk); $this->em->flush(); - $this->assertEquals('food/milk/fructis', $fruits->getSlug()); + static::assertSame('food/milk/fructis', $fruits->getSlug()); - $oranges = $repo->findOneByTitle('Oranges'); - $this->assertEquals('food/milk/fructis/oranges', $oranges->getSlug()); + $oranges = $repo->findOneBy(['title' => 'Oranges']); + static::assertSame('food/milk/fructis/oranges', $oranges->getSlug()); - $citrons = $repo->findOneByTitle('Citrons'); - $this->assertEquals('food/milk/fructis/citrons', $citrons->getSlug()); + $citrons = $repo->findOneBy(['title' => 'Citrons']); + static::assertSame('food/milk/fructis/citrons', $citrons->getSlug()); - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $food->setTitle('Foodissimo'); $this->em->persist($food); $this->em->flush(); - $this->assertEquals('foodissimo', $food->getSlug()); - $this->assertEquals('foodissimo/milk/fructis/oranges', $oranges->getSlug()); - $this->assertEquals('foodissimo/milk/fructis/citrons', $citrons->getSlug()); + static::assertSame('foodissimo', $food->getSlug()); + static::assertSame('foodissimo/milk/fructis/oranges', $oranges->getSlug()); + static::assertSame('foodissimo/milk/fructis/citrons', $citrons->getSlug()); $repo->persistAsFirstChildOf($fruits, $food); $this->em->flush(); - $this->assertEquals('foodissimo/fructis/oranges', $oranges->getSlug()); - $this->assertEquals('foodissimo/fructis/citrons', $citrons->getSlug()); + static::assertSame('foodissimo/fructis/oranges', $oranges->getSlug()); + static::assertSame('foodissimo/fructis/citrons', $citrons->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TARGET, - ); + return [ + TreeSlug::class, + ]; } - private function populate() + private function populate(): void { - $repo = $this->em->getRepository(self::TARGET); + $repo = $this->em->getRepository(TreeSlug::class); $food = new TreeSlug(); $food->setTitle('Food'); diff --git a/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerUniqueTest.php b/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerUniqueTest.php new file mode 100644 index 0000000000..f11e550e25 --- /dev/null +++ b/tests/Gedmo/Sluggable/Handlers/TreeSlugHandlerUniqueTest.php @@ -0,0 +1,79 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Handlers; + +use Doctrine\Common\EventManager; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Handler\TreeSlug; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tree\TreeListener; + +final class TreeSlugHandlerUniqueTest extends BaseTestCaseORM +{ + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $evm->addEventSubscriber(new SluggableListener()); + $evm->addEventSubscriber(new TreeListener()); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testUniqueRoot(): void + { + $foo1 = new TreeSlug(); + $foo1->setTitle('Foo'); + + $foo2 = new TreeSlug(); + $foo2->setTitle('Foo'); + + $this->em->persist($foo1); + $this->em->persist($foo2); + + $this->em->flush(); + + static::assertSame('foo', $foo1->getSlug()); + static::assertSame('foo-1', $foo2->getSlug()); + } + + public function testUniqueLeaf(): void + { + $root = new TreeSlug(); + $root->setTitle('root'); + + $foo1 = new TreeSlug(); + $foo1->setTitle('Foo'); + $foo1->setParent($root); + + $foo2 = new TreeSlug(); + $foo2->setTitle('Foo'); + $foo2->setParent($root); + + $this->em->persist($root); + $this->em->persist($foo1); + $this->em->persist($foo2); + + $this->em->flush(); + + static::assertSame('root/foo', $foo1->getSlug()); + static::assertSame('root/foo-1', $foo2->getSlug()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + TreeSlug::class, + ]; + } +} diff --git a/tests/Gedmo/Sluggable/Handlers/UserRelativeSlugHandlerTest.php b/tests/Gedmo/Sluggable/Handlers/UserRelativeSlugHandlerTest.php index 29c379e6d1..da87576475 100644 --- a/tests/Gedmo/Sluggable/Handlers/UserRelativeSlugHandlerTest.php +++ b/tests/Gedmo/Sluggable/Handlers/UserRelativeSlugHandlerTest.php @@ -1,35 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Handlers; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Handler\User; -use Sluggable\Fixture\Handler\Company; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Handler\Company; +use Gedmo\Tests\Sluggable\Fixture\Handler\User; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class UserRelativeSlugHandlerTest extends BaseTestCaseORM +final class UserRelativeSlugHandlerTest extends BaseTestCaseORM { - const USER = "Sluggable\\Fixture\\Handler\\User"; - const COMPANY = "Sluggable\\Fixture\\Handler\\Company"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testRelativeSlug() + public function testRelativeSlug(): void { $company = new Company(); $company->setTitle('KnpLabs'); @@ -42,20 +47,20 @@ public function testRelativeSlug() $this->em->flush(); - $this->assertEquals('knplabs/gedi', $gedi->getSlug(), 'relative slug is invalid'); + static::assertSame('knplabs/gedi', $gedi->getSlug(), 'relative slug is invalid'); $company->setTitle('KnpLabs Nantes'); $this->em->persist($company); $this->em->flush(); - $this->assertEquals('knplabs-nantes/gedi', $gedi->getSlug(), 'relative slug is invalid'); + static::assertSame('knplabs-nantes/gedi', $gedi->getSlug(), 'relative slug is invalid'); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::USER, - self::COMPANY, - ); + return [ + User::class, + Company::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Inheritance2Test.php b/tests/Gedmo/Sluggable/Inheritance2Test.php deleted file mode 100644 index bd4c608147..0000000000 --- a/tests/Gedmo/Sluggable/Inheritance2Test.php +++ /dev/null @@ -1,61 +0,0 @@ -addEventSubscriber(new SluggableListener()); - - $this->getMockSqliteEntityManager($evm); - } - - public function testSlugGeneration() - { - $audi = new Car(); - $audi->setDescription('audi car'); - $audi->setTitle('Audi'); - - $this->em->persist($audi); - - $audi2 = new Car(); - $audi2->setDescription('audi2 car'); - $audi2->setTitle('Audi'); - - $this->em->persist($audi2); - - $audi3 = new SportCar(); - $audi3->setDescription('audi3 car'); - $audi3->setTitle('Audi'); - - $this->em->persist($audi3); - $this->em->flush(); - } - - protected function getUsedEntityFixtures() - { - return array( - self::VEHICLE, - self::CAR, - self::SPORTCAR, - ); - } -} diff --git a/tests/Gedmo/Sluggable/InheritanceTest.php b/tests/Gedmo/Sluggable/InheritanceTest.php deleted file mode 100644 index a30fa817ce..0000000000 --- a/tests/Gedmo/Sluggable/InheritanceTest.php +++ /dev/null @@ -1,60 +0,0 @@ - - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class InheritanceTest extends BaseTestCaseORM -{ - const VEHICLE = 'Sluggable\\Fixture\\Inheritance\\Vehicle'; - const CAR = 'Sluggable\\Fixture\\Inheritance\\Car'; - - protected function setUp() - { - parent::setUp(); - - $evm = new EventManager(); - $evm->addEventSubscriber(new SluggableListener()); - - $this->getMockSqliteEntityManager($evm); - } - - public function testSlugGeneration() - { - $audi = new Car(); - $audi->setDescription('audi car'); - $audi->setTitle('Audi'); - - $this->em->persist($audi); - - $audi2 = new Car(); - $audi2->setDescription('audi2 car'); - $audi2->setTitle('Audi'); - - $this->em->persist($audi2); - - $audi3 = new Vehicle(); - $audi3->setTitle('Audi'); - - $this->em->persist($audi3); - $this->em->flush(); - } - - protected function getUsedEntityFixtures() - { - return array( - self::VEHICLE, - self::CAR, - ); - } -} diff --git a/tests/Gedmo/Sluggable/Issue/Issue100Test.php b/tests/Gedmo/Sluggable/Issue/Issue100Test.php new file mode 100644 index 0000000000..ea2710760b --- /dev/null +++ b/tests/Gedmo/Sluggable/Issue/Issue100Test.php @@ -0,0 +1,103 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; + +use Doctrine\Common\EventManager; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Issue100\Article; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; + +/** + * These are tests for sluggable behavior + * + * @author Gediminas Morkevicius + */ +final class Issue100Test extends BaseTestCaseORM +{ + private TranslatableListener $translatableListener; + + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $evm->addEventSubscriber(new SluggableListener()); + $this->translatableListener = new TranslatableListener(); + $this->translatableListener->setTranslatableLocale('en'); + $this->translatableListener->setDefaultLocale('en'); + $evm->addEventSubscriber($this->translatableListener); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testShouldWorkWithTranslatableSlug(): void + { + $repository = $this->em->getRepository(Translation::class); + + /* + * First article + */ + $article = new Article(); + $article->setTitle('First Article'); + $this->em->persist($article); + + /* + * Second article + */ + $article2 = new Article(); + $article2->setTitle('First Article'); + $this->em->persist($article2); + + $this->em->flush(); + + $this->translatableListener->setTranslatableLocale('fr'); + + $article->setTitle('Premier article'); + $this->em->flush(); + + $article2->setTitle('Premier article'); + $this->em->flush(); + + $this->translatableListener->setTranslatableLocale('en'); + + $this->em->refresh($article); + $this->em->refresh($article2); + + static::assertSame('first-article', $article->getSlug()); + static::assertSame('first-article-1', $article2->getSlug()); + + $translations = $repository->findTranslations($article); + static::assertArrayHasKey('fr', $translations); + static::assertArrayHasKey('title', $translations['fr']); + static::assertArrayHasKey('slug', $translations['fr']); + static::assertSame('premier-article', $translations['fr']['slug']); + + $translations2 = $repository->findTranslations($article2); + static::assertArrayHasKey('fr', $translations2); + static::assertArrayHasKey('title', $translations2['fr']); + static::assertArrayHasKey('slug', $translations2['fr']); + + // This should be 'premier-article-1' instead of 'premier-article' because of using + // TranslationWalker hint in `getSimilarSlugs`method + static::assertSame('premier-article-1', $translations2['fr']['slug']); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + Translation::class, + ]; + } +} diff --git a/tests/Gedmo/Sluggable/Issue/Issue104Test.php b/tests/Gedmo/Sluggable/Issue/Issue104Test.php index 9e45de2baf..6894cab908 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue104Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue104Test.php @@ -1,36 +1,43 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Issue104\Car; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Issue104\Car; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue104Test extends BaseTestCaseORM +final class Issue104Test extends BaseTestCaseORM { - const CAR = 'Sluggable\\Fixture\\Issue104\\Car'; - - protected function setUp() + public static function setUpBeforeClass(): void { - parent::setUp(); + if (!class_exists(AnnotationDriver::class)) { + static::markTestSkipped('Test validates checks for invalid mapping configuration which have changed between ORM 2.x and 3.x causing the ORM to abort before reaching our checks.'); + } } - /** - * @test - * @expectedException Gedmo\Exception\InvalidMappingException - */ - public function shouldThrowAnExceptionWhenMappedSuperclassProtectedProperty() + public function testShouldThrowAnExceptionWhenMappedSuperclassProtectedProperty(): void { + $this->expectException(InvalidMappingException::class); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $audi = new Car(); $audi->setDescription('audi car'); @@ -40,10 +47,10 @@ public function shouldThrowAnExceptionWhenMappedSuperclassProtectedProperty() $this->em->flush(); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CAR, - ); + return [ + Car::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Issue/Issue1058Test.php b/tests/Gedmo/Sluggable/Issue/Issue1058Test.php index 6524d66de6..f9ee6f1214 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue1058Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue1058Test.php @@ -1,39 +1,43 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Issue1058\Page; -use Sluggable\Fixture\Issue1058\User; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Issue1058\Page; +use Gedmo\Tests\Sluggable\Fixture\Issue1058\User; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue1058Test extends BaseTestCaseORM +final class Issue1058Test extends BaseTestCaseORM { - const ARTICLE = 'Sluggable\\Fixture\\Issue1058\\Page'; - const USER = 'Sluggable\\Fixture\\Issue1058\\User'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); - $evm = new EventManager; - $evm->addEventSubscriber(new SluggableListener); + $evm = new EventManager(); + $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } /** - * @test * @group issue1058 */ - public function shouldHandleUniqueConstraintsBasedOnRelation() + public function testShouldHandleUniqueConstraintsBasedOnRelation(): void { $userFoo = new User(); $this->em->persist($userFoo); @@ -49,7 +53,7 @@ public function shouldHandleUniqueConstraintsBasedOnRelation() $this->em->persist($page); $this->em->flush(); - $this->assertEquals('the-title', $page->getSlug()); + static::assertSame('the-title', $page->getSlug()); $page = new Page(); $page->setTitle('the title'); @@ -57,7 +61,7 @@ public function shouldHandleUniqueConstraintsBasedOnRelation() $this->em->persist($page); $this->em->flush(); - $this->assertEquals('the-title', $page->getSlug()); + static::assertSame('the-title', $page->getSlug()); $page = new Page(); $page->setTitle('the title'); @@ -65,7 +69,7 @@ public function shouldHandleUniqueConstraintsBasedOnRelation() $this->em->persist($page); $this->em->flush(); - $this->assertEquals('the-title-1', $page->getSlug()); + static::assertSame('the-title-1', $page->getSlug()); $page = new Page(); $page->setTitle('the title'); @@ -75,14 +79,14 @@ public function shouldHandleUniqueConstraintsBasedOnRelation() $this->em->flush(); $this->em->clear(); - $this->assertEquals('the-title-1', $page->getSlug()); + static::assertSame('the-title-1', $page->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::USER - ); + return [ + Page::class, + User::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Issue/Issue1151Test.php b/tests/Gedmo/Sluggable/Issue/Issue1151Test.php index 52e4a55506..276531f6f8 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue1151Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue1151Test.php @@ -1,41 +1,51 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Sluggable\Fixture\Issue1151\Article; -use Tool\BaseTestCaseMongoODM; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Issue1151\Article; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * Gedmo\Sluggable\Issue1151Test * * @author Vaidas Laลพauskas */ -class Issue1151Test extends BaseTestCaseMongoODM +final class Issue1151Test extends BaseTestCaseMongoODM { /** - * Test if new object with predefined id will be processed by sluggable listener + * Set up test */ - public function testSlugCreateOnNewArticle() + protected function setUp(): void { - $article = new Article(); - $article->setId('ABC123'); - $article->setTitle('Test'); - $this->dm->persist($article); + parent::setUp(); + $evm = new EventManager(); + $evm->addEventSubscriber(new SluggableListener()); - $this->dm->flush(); - $this->assertEquals('test', $article->getSlug()); + $this->getMockDocumentManager($evm); } /** - * Set up test + * Test if new object with predefined id will be processed by sluggable listener */ - protected function setUp() + public function testSlugCreateOnNewArticle(): void { - parent::setUp(); - $evm = new EventManager(); - $evm->addEventSubscriber(new SluggableListener()); + $article = new Article(); + $article->setId('ABC123'); + $article->setTitle('Test'); + $this->dm->persist($article); - $this->getMockDocumentManager($evm); + $this->dm->flush(); + static::assertSame('test', $article->getSlug()); } } diff --git a/tests/Gedmo/Sluggable/Issue/Issue116Test.php b/tests/Gedmo/Sluggable/Issue/Issue116Test.php index c08cf224f6..755a271fe4 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue116Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue116Test.php @@ -1,46 +1,43 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Issue116\Country; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\ORM\Mapping\Driver\YamlDriver; -use Doctrine\ORM\Mapping\Driver\DriverChain; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Issue116\Country; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue116Test extends BaseTestCaseORM +final class Issue116Test extends BaseTestCaseORM { - const TARGET = 'Sluggable\\Fixture\\Issue116\\Country'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - protected function getMetadataDriverImplementation() - { - $chain = new DriverChain(); - $chain->addDriver( - new YamlDriver(array(__DIR__.'/../Fixture/Issue116/Mapping')), - 'Sluggable\Fixture\Issue116' - ); - - return $chain; - } - - public function testSlugGeneration() + public function testSlugGeneration(): void { $country = new Country(); $country->setOriginalName('New Zealand'); @@ -48,13 +45,29 @@ public function testSlugGeneration() $this->em->persist($country); $this->em->flush(); - $this->assertEquals('new-zealand', $country->getAlias()); + static::assertSame('new-zealand', $country->getAlias()); + } + + protected function getMetadataDriverImplementation(): MappingDriver + { + $chain = new MappingDriverChain(); + + if (PHP_VERSION_ID >= 80000) { + $chain->addDriver(new AttributeDriver([]), 'Gedmo\Tests\Sluggable\Fixture\Issue116'); + } else { + $chain->addDriver( + new YamlDriver([__DIR__.'/../Fixture/Issue116/Mapping']), + 'Gedmo\Tests\Sluggable\Fixture\Issue116' + ); + } + + return $chain; } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TARGET, - ); + return [ + Country::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Issue/Issue1177Test.php b/tests/Gedmo/Sluggable/Issue/Issue1177Test.php index 1c600ae459..e457613a98 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue1177Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue1177Test.php @@ -1,36 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Issue1177\Article; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Issue1177\Article; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue1177Test extends BaseTestCaseORM +final class Issue1177Test extends BaseTestCaseORM { - const ARTICLE = 'Sluggable\\Fixture\\Issue1177\\Article'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); - $evm = new EventManager; - $evm->addEventSubscriber(new SluggableListener); + $evm = new EventManager(); + $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldTryPreferedSlugFirst() + public function testShouldTryPreferedSlugFirst(): void { $article = new Article(); $article->setTitle('the title with number 1'); @@ -38,7 +41,7 @@ public function shouldTryPreferedSlugFirst() $this->em->persist($article); $this->em->flush(); $this->em->clear(); - $this->assertEquals('the-title-with-number-1', $article->getSlug()); + static::assertSame('the-title-with-number-1', $article->getSlug()); $article = new Article(); $article->setTitle('the title with number'); @@ -48,7 +51,7 @@ public function shouldTryPreferedSlugFirst() $this->em->clear(); // the slug was 'the-title-with-number-2' before the fix here // despite the fact that there is no entity with slug 'the-title-with-number' - $this->assertEquals('the-title-with-number', $article->getSlug()); + static::assertSame('the-title-with-number', $article->getSlug()); $article = new Article(); $article->setTitle('the title with number'); @@ -56,13 +59,13 @@ public function shouldTryPreferedSlugFirst() $this->em->persist($article); $this->em->flush(); $this->em->clear(); - $this->assertEquals('the-title-with-number-2', $article->getSlug()); + static::assertSame('the-title-with-number-2', $article->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - ); + return [ + Article::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Issue/Issue1240Test.php b/tests/Gedmo/Sluggable/Issue/Issue1240Test.php index d145c8600b..05d7d2f8f6 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue1240Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue1240Test.php @@ -1,36 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Issue1240\Article; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Issue1240\Article; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue1240Test extends BaseTestCaseORM +final class Issue1240Test extends BaseTestCaseORM { - const ARTICLE = 'Sluggable\\Fixture\\Issue1240\\Article'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); - $evm = new EventManager; - $evm->addEventSubscriber(new SluggableListener); + $evm = new EventManager(); + $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldWorkWithPlusAsSeparator() + public function testShouldWorkWithPlusAsSeparator(): void { $article = new Article(); $article->setTitle('the title'); @@ -43,11 +46,11 @@ public function shouldWorkWithPlusAsSeparator() $this->em->flush(); $this->em->clear(); - $this->assertEquals('the+title', $article->getSlug()); - $this->assertEquals('The+Title', $article->getCamelSlug()); + static::assertSame('the+title', $article->getSlug()); + static::assertSame('The+Title', $article->getCamelSlug()); - $this->assertEquals('the+title+1', $article2->getSlug()); - $this->assertEquals('The+Title+1', $article2->getCamelSlug()); + static::assertSame('the+title+1', $article2->getSlug()); + static::assertSame('The+Title+1', $article2->getCamelSlug()); $article = new Article(); $article->setTitle('the title'); @@ -55,14 +58,14 @@ public function shouldWorkWithPlusAsSeparator() $this->em->persist($article); $this->em->flush(); $this->em->clear(); - $this->assertEquals('the+title+2', $article->getSlug()); - $this->assertEquals('The+Title+2', $article->getCamelSlug()); + static::assertSame('the+title+2', $article->getSlug()); + static::assertSame('The+Title+2', $article->getCamelSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - ); + return [ + Article::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Issue/Issue131Test.php b/tests/Gedmo/Sluggable/Issue/Issue131Test.php index 0f0e13e14b..094b9cc643 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue131Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue131Test.php @@ -1,33 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Issue131\Article; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Issue131\Article; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue131Test extends BaseTestCaseORM +final class Issue131Test extends BaseTestCaseORM { - const TARGET = 'Sluggable\\Fixture\\Issue131\\Article'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testSlugGeneration() + public function testSlugGeneration(): void { $test = new Article(); $test->setTitle(''); @@ -35,7 +41,7 @@ public function testSlugGeneration() $this->em->persist($test); $this->em->flush(); - $this->assertNull($test->getSlug()); + static::assertNull($test->getSlug()); $test2 = new Article(); $test2->setTitle(''); @@ -43,13 +49,24 @@ public function testSlugGeneration() $this->em->persist($test2); $this->em->flush(); - $this->assertNull($test2->getSlug()); + static::assertNull($test2->getSlug()); + } + + public function testShouldHandleOnlyZeroInSlug(): void + { + $article = new Article(); + $article->setTitle('0'); + + $this->em->persist($article); + $this->em->flush(); + + static::assertSame('0', $article->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TARGET, - ); + return [ + Article::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Issue/Issue449Test.php b/tests/Gedmo/Sluggable/Issue/Issue449Test.php index fd605f5786..7cc6dd056b 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue449Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue449Test.php @@ -1,27 +1,37 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter; use Gedmo\SoftDeleteable\SoftDeleteableListener; -use Sluggable\Fixture\Issue449\Article; +use Gedmo\Tests\Sluggable\Fixture\Issue449\Article; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Craig Marvelley - * @link http://marvelley.com - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @see http://marvelley.com */ -class Issue449Test extends BaseTestCaseORM +final class Issue449Test extends BaseTestCaseORM { - const TARGET = 'Sluggable\\Fixture\\Issue449\\Article'; - const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable'; + private const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable'; - private $softDeleteableListener; + private SoftDeleteableListener $softDeleteableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -33,25 +43,15 @@ protected function setUp() $this->softDeleteableListener = new SoftDeleteableListener(); $evm->addEventSubscriber($this->softDeleteableListener); - $config = $this->getMockAnnotatedConfig(); - $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, 'Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter'); + $config = $this->getDefaultConfiguration(); + $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, SoftDeleteableFilter::class); - $this->em = $this->getMockSqliteEntityManager($evm, $config); + $this->em = $this->getDefaultMockSqliteEntityManager($evm, $config); $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); } - protected function getUsedEntityFixtures() - { - return array( - self::TARGET, - ); - } - - /** - * @test - */ - public function shouldBuildUniqueSlugAfterSoftDeleteFilterIsDisabled() + public function testShouldBuildUniqueSlugAfterSoftDeleteFilterIsDisabled(): void { $article = new Article(); $article->setTitle('the soft title'); @@ -73,6 +73,13 @@ public function shouldBuildUniqueSlugAfterSoftDeleteFilterIsDisabled() $this->em->flush(); $this->em->clear(); - $this->assertNotEquals($slug, $article->getSlug()); + static::assertNotSame($slug, $article->getSlug()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Issue/Issue633Test.php b/tests/Gedmo/Sluggable/Issue/Issue633Test.php index 561c7ae3e9..34908fa48a 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue633Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue633Test.php @@ -1,36 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Issue633\Article; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Issue633\Article; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Derek Clapham - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue633Test extends BaseTestCaseORM +final class Issue633Test extends BaseTestCaseORM { - const TARGET = 'Sluggable\\Fixture\\Issue633\\Article'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldHandleUniqueBasedSlug() + public function testShouldHandleUniqueBasedSlug(): void { $test = new Article(); $test->setTitle('Unique to code'); @@ -39,7 +42,7 @@ public function shouldHandleUniqueBasedSlug() $this->em->persist($test); $this->em->flush(); - $this->assertEquals('unique-to-code', $test->getSlug()); + static::assertSame('unique-to-code', $test->getSlug()); $test2 = new Article(); $test2->setTitle('Unique to code'); @@ -48,7 +51,7 @@ public function shouldHandleUniqueBasedSlug() $this->em->persist($test2); $this->em->flush(); - $this->assertEquals('unique-to-code', $test2->getSlug()); + static::assertSame('unique-to-code', $test2->getSlug()); $test3 = new Article(); $test3->setTitle('Unique to code'); @@ -57,13 +60,10 @@ public function shouldHandleUniqueBasedSlug() $this->em->persist($test3); $this->em->flush(); - $this->assertEquals('unique-to-code-1', $test3->getSlug()); + static::assertSame('unique-to-code-1', $test3->getSlug()); } - /** - * @test - */ - public function handlePersistedSlugsForUniqueBased() + public function testHandlePersistedSlugsForUniqueBased(): void { $test = new Article(); $test->setTitle('Unique to code'); @@ -84,15 +84,15 @@ public function handlePersistedSlugsForUniqueBased() $this->em->persist($test3); $this->em->flush(); - $this->assertEquals('unique-to-code', $test->getSlug()); - $this->assertEquals('unique-to-code', $test2->getSlug()); - $this->assertEquals('unique-to-code-1', $test3->getSlug()); + static::assertSame('unique-to-code', $test->getSlug()); + static::assertSame('unique-to-code', $test2->getSlug()); + static::assertSame('unique-to-code-1', $test3->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TARGET, - ); + return [ + Article::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Issue/Issue827Test.php b/tests/Gedmo/Sluggable/Issue/Issue827Test.php index 0d8cbaf194..9dd2245a88 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue827Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue827Test.php @@ -1,43 +1,47 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Issue827\Article; -use Sluggable\Fixture\Issue827\Category; -use Sluggable\Fixture\Issue827\Comment; -use Sluggable\Fixture\Issue827\Post; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Issue827\Article; +use Gedmo\Tests\Sluggable\Fixture\Issue827\Category; +use Gedmo\Tests\Sluggable\Fixture\Issue827\Comment; +use Gedmo\Tests\Sluggable\Fixture\Issue827\Post; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Anders S. ร˜fsdahl - * @link http://www.aloof.no - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @see http://www.aloof.no */ -class Issue827Test extends BaseTestCaseORM +final class Issue827Test extends BaseTestCaseORM { - const ARTICLE = 'Sluggable\\Fixture\\Issue827\\Article'; - const CATEGORY = 'Sluggable\\Fixture\\Issue827\\Category'; - const COMMENT = 'Sluggable\\Fixture\\Issue827\\Comment'; - const POST = 'Sluggable\\Fixture\\Issue827\\Post'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } /** - * @test * @group issue827 */ - public function shouldHandleForeignKeyUniqueBasedSlug() + public function testShouldHandleForeignKeyUniqueBasedSlug(): void { // Creating categories @@ -46,21 +50,21 @@ public function shouldHandleForeignKeyUniqueBasedSlug() $this->em->persist($testCat1); $this->em->flush(); - $this->assertEquals('category1', $testCat1->getSlug()); + static::assertSame('category1', $testCat1->getSlug()); $testCat11 = new Category(); $testCat11->setTitle('Category1'); $this->em->persist($testCat11); $this->em->flush(); - $this->assertEquals('category1-1', $testCat11->getSlug()); + static::assertSame('category1-1', $testCat11->getSlug()); $testCat2 = new Category(); $testCat2->setTitle('Category2'); $this->em->persist($testCat2); $this->em->flush(); - $this->assertEquals('category2', $testCat2->getSlug()); + static::assertSame('category2', $testCat2->getSlug()); // Creating articles @@ -70,7 +74,7 @@ public function shouldHandleForeignKeyUniqueBasedSlug() $this->em->persist($test); $this->em->flush(); - $this->assertEquals('unique-to-category-1', $test->getSlug()); + static::assertSame('unique-to-category-1', $test->getSlug()); $test2 = new Article(); $test2->setTitle('Unique to category 2'); @@ -78,7 +82,7 @@ public function shouldHandleForeignKeyUniqueBasedSlug() $this->em->persist($test2); $this->em->flush(); - $this->assertEquals('unique-to-category-2', $test2->getSlug()); + static::assertSame('unique-to-category-2', $test2->getSlug()); $test3 = new Article(); $test3->setTitle('Unique to category 1'); @@ -86,14 +90,13 @@ public function shouldHandleForeignKeyUniqueBasedSlug() $this->em->persist($test3); $this->em->flush(); - $this->assertEquals('unique-to-category-1-1', $test3->getSlug()); + static::assertSame('unique-to-category-1-1', $test3->getSlug()); } /** - * @test * @group issue827 */ - public function handlePersistedSlugsForForeignKeyUniqueBased() + public function testHandlePersistedSlugsForForeignKeyUniqueBased(): void { // Creating categories @@ -128,19 +131,18 @@ public function handlePersistedSlugsForForeignKeyUniqueBased() $this->em->flush(); - $this->assertEquals('category1', $testCat1->getSlug()); - $this->assertEquals('category1-1', $testCat11->getSlug()); - $this->assertEquals('category2', $testCat2->getSlug()); - $this->assertEquals('unique-to-category-1', $test->getSlug()); - $this->assertEquals('unique-to-category-2', $test2->getSlug()); - $this->assertEquals('unique-to-category-1-1', $test3->getSlug()); + static::assertSame('category1', $testCat1->getSlug()); + static::assertSame('category1-1', $testCat11->getSlug()); + static::assertSame('category2', $testCat2->getSlug()); + static::assertSame('unique-to-category-1', $test->getSlug()); + static::assertSame('unique-to-category-2', $test2->getSlug()); + static::assertSame('unique-to-category-1-1', $test3->getSlug()); } /** - * @test * @group issue827 */ - public function shouldHandleForeignKeyMultipleColumnsUniqueBasedSlug() + public function testShouldHandleForeignKeyMultipleColumnsUniqueBasedSlug(): void { // Creating parents @@ -149,25 +151,25 @@ public function shouldHandleForeignKeyMultipleColumnsUniqueBasedSlug() $this->em->persist($testPost1); $this->em->flush(); - $this->assertEquals('post-1', $testPost1->getSlug()); + static::assertSame('post-1', $testPost1->getSlug()); $testPost2 = new Post(); $testPost2->setTitle('Post 2'); $this->em->persist($testPost2); $this->em->flush(); - $this->assertEquals('post-2', $testPost2->getSlug()); + static::assertSame('post-2', $testPost2->getSlug()); // we have to refresh entities to ensure that Doctrine are aware of the sluggable generated identifiers $this->em->clear(); $testPost1 = $this->em->find( - self::POST, - array('title' => $testPost1->getTitle(), 'slug' => $testPost1->getSlug()) + Post::class, + ['title' => $testPost1->getTitle(), 'slug' => $testPost1->getSlug()] ); $testPost2 = $this->em->find( - self::POST, - array('title' => $testPost2->getTitle(), 'slug' => $testPost2->getSlug()) + Post::class, + ['title' => $testPost2->getTitle(), 'slug' => $testPost2->getSlug()] ); // Creating comments @@ -178,7 +180,7 @@ public function shouldHandleForeignKeyMultipleColumnsUniqueBasedSlug() $this->em->persist($test); $this->em->flush(); - $this->assertEquals('unique-to-post-1', $test->getSlug()); + static::assertSame('unique-to-post-1', $test->getSlug()); $test2 = new Comment(); $test2->setTitle('Unique to post 2'); @@ -186,7 +188,7 @@ public function shouldHandleForeignKeyMultipleColumnsUniqueBasedSlug() $this->em->persist($test2); $this->em->flush(); - $this->assertEquals('unique-to-post-2', $test2->getSlug()); + static::assertSame('unique-to-post-2', $test2->getSlug()); $test3 = new Comment(); $test3->setTitle('Unique to post 1'); @@ -194,7 +196,7 @@ public function shouldHandleForeignKeyMultipleColumnsUniqueBasedSlug() $this->em->persist($test3); $this->em->flush(); - $this->assertEquals('unique-to-post-1-1', $test3->getSlug()); + static::assertSame('unique-to-post-1-1', $test3->getSlug()); $test4 = new Comment(); $test4->setTitle('Unique to post 1'); @@ -202,7 +204,7 @@ public function shouldHandleForeignKeyMultipleColumnsUniqueBasedSlug() $this->em->persist($test4); $this->em->flush(); - $this->assertEquals('unique-to-post-1-2', $test4->getSlug()); + static::assertSame('unique-to-post-1-2', $test4->getSlug()); $test5 = new Comment(); $test5->setTitle('Unique to post 2'); @@ -210,14 +212,13 @@ public function shouldHandleForeignKeyMultipleColumnsUniqueBasedSlug() $this->em->persist($test5); $this->em->flush(); - $this->assertEquals('unique-to-post-2-1', $test5->getSlug()); + static::assertSame('unique-to-post-2-1', $test5->getSlug()); } /** - * @test * @group issue827 */ - public function handlePersistedForeignKeyMultipleColumnsUniqueBasedSlug() + public function testHandlePersistedForeignKeyMultipleColumnsUniqueBasedSlug(): void { // Creating parents @@ -258,22 +259,22 @@ public function handlePersistedForeignKeyMultipleColumnsUniqueBasedSlug() $this->em->flush(); - $this->assertEquals('post-1', $testPost1->getSlug()); - $this->assertEquals('post-2', $testPost2->getSlug()); - $this->assertEquals('unique-to-post-1', $test->getSlug()); - $this->assertEquals('unique-to-post-2', $test2->getSlug()); - $this->assertEquals('unique-to-post-1-1', $test3->getSlug()); - $this->assertEquals('unique-to-post-1-2', $test4->getSlug()); - $this->assertEquals('unique-to-post-2-1', $test5->getSlug()); + static::assertSame('post-1', $testPost1->getSlug()); + static::assertSame('post-2', $testPost2->getSlug()); + static::assertSame('unique-to-post-1', $test->getSlug()); + static::assertSame('unique-to-post-2', $test2->getSlug()); + static::assertSame('unique-to-post-1-1', $test3->getSlug()); + static::assertSame('unique-to-post-1-2', $test4->getSlug()); + static::assertSame('unique-to-post-2-1', $test5->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::CATEGORY, - self::COMMENT, - self::POST, - ); + return [ + Article::class, + Category::class, + Comment::class, + Post::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/Issue/Issue939Test.php b/tests/Gedmo/Sluggable/Issue/Issue939Test.php index e2ff85ab6a..145872106f 100644 --- a/tests/Gedmo/Sluggable/Issue/Issue939Test.php +++ b/tests/Gedmo/Sluggable/Issue/Issue939Test.php @@ -1,36 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Issue939\SluggableListener as SluggableListenerIssue939; -use Sluggable\Fixture\Issue939\Article; -use Sluggable\Fixture\Issue939\Category; +use Gedmo\Tests\Sluggable\Fixture\Issue939\Article; +use Gedmo\Tests\Sluggable\Fixture\Issue939\Category; +use Gedmo\Tests\Sluggable\Fixture\Issue939\SluggableListener as SluggableListenerIssue939; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue939Test extends BaseTestCaseORM +final class Issue939Test extends BaseTestCaseORM { - const ARTICLE = 'Sluggable\\Fixture\\Issue939\\Article'; - const CATEGORY = 'Sluggable\\Fixture\\Issue939\\Category'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListenerIssue939()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testSlugGeneration() + public function testSlugGeneration(): void { $category = new Category(); $category->setTitle('Misc articles'); @@ -43,15 +47,15 @@ public function testSlugGeneration() $this->em->persist($article); $this->em->flush(); - $this->assertEquals('Is there water on the moon?', $article->getSlug()); - $this->assertEquals('misc-articles', $category->getSlug()); + static::assertSame('Is there water on the moon?', $article->getSlug()); + static::assertSame('misc-articles', $category->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::CATEGORY, - ); + return [ + Article::class, + Category::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/MappedSuperclassTest.php b/tests/Gedmo/Sluggable/MappedSuperclassTest.php deleted file mode 100644 index 76ff849215..0000000000 --- a/tests/Gedmo/Sluggable/MappedSuperclassTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class MappedSuperclassTest extends BaseTestCaseORM -{ - const CAR = 'Sluggable\\Fixture\\MappedSuperclass\\Car'; - - protected function setUp() - { - parent::setUp(); - } - - /** - * If the MappedSuperclass doesn't have an identifier, SluggableListener generates a notice - * Undefined offset: 0 in Doctrine/ORM/Mapping/ClassMetadataInfo.php:986 - * @test - */ - public function shouldntGenerateNotice() - { - $evm = new EventManager(); - $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); - - $audi = new Car(); - $audi->setDescription('audi car'); - $audi->setTitle('Audi'); - - $this->em->persist($audi); - $this->em->flush(); - } - - protected function getUsedEntityFixtures() - { - return array( - self::CAR, - ); - } -} diff --git a/tests/Gedmo/Sluggable/SluggableConfigurationTest.php b/tests/Gedmo/Sluggable/SluggableConfigurationTest.php index 47aefad828..e56c204327 100644 --- a/tests/Gedmo/Sluggable/SluggableConfigurationTest.php +++ b/tests/Gedmo/Sluggable/SluggableConfigurationTest.php @@ -1,46 +1,53 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\ConfigurationArticle; +use Gedmo\Sluggable\Sluggable; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\ConfigurationArticle; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SluggableConfigurationTest extends BaseTestCaseORM +final class SluggableConfigurationTest extends BaseTestCaseORM { - const ARTICLE = 'Sluggable\\Fixture\\ConfigurationArticle'; - - private $articleId; + private ?int $articleId = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testInsertedNewSlug() + public function testInsertedNewSlug(): void { - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(ConfigurationArticle::class, $this->articleId); - $this->assertTrue($article instanceof Sluggable); - $this->assertEquals('the-title-my-code', $article->getSlug()); + static::assertInstanceOf(Sluggable::class, $article); + static::assertSame('the-title-my-code', $article->getSlug()); } - public function testNonUniqueSlugGeneration() + public function testNonUniqueSlugGeneration(): void { - for ($i = 0; $i < 5; $i++) { + for ($i = 0; $i < 5; ++$i) { $article = new ConfigurationArticle(); $article->setTitle('the title'); $article->setCode('my code'); @@ -48,11 +55,11 @@ public function testNonUniqueSlugGeneration() $this->em->persist($article); $this->em->flush(); $this->em->clear(); - $this->assertEquals('the-title-my-code', $article->getSlug()); + static::assertSame('the-title-my-code', $article->getSlug()); } } - public function testSlugLimit() + public function testSlugLimit(): void { $long = 'the title the title the title the title the'; $article = new ConfigurationArticle(); @@ -64,28 +71,28 @@ public function testSlugLimit() $this->em->clear(); $shorten = $article->getSlug(); - $this->assertEquals(32, strlen($shorten)); + static::assertSame(32, strlen($shorten)); } - public function testNonUpdatableSlug() + public function testNonUpdatableSlug(): void { - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(ConfigurationArticle::class, $this->articleId); $article->setTitle('the title updated'); $this->em->persist($article); $this->em->flush(); $this->em->clear(); - $this->assertEquals('the-title-my-code', $article->getSlug()); + static::assertSame('the-title-my-code', $article->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - ); + return [ + ConfigurationArticle::class, + ]; } - private function populate() + private function populate(): void { $article = new ConfigurationArticle(); $article->setTitle('the title'); diff --git a/tests/Gedmo/Sluggable/SluggableDateTimeTypesTest.php b/tests/Gedmo/Sluggable/SluggableDateTimeTypesTest.php new file mode 100644 index 0000000000..19d62050bb --- /dev/null +++ b/tests/Gedmo/Sluggable/SluggableDateTimeTypesTest.php @@ -0,0 +1,110 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; + +use Doctrine\Common\EventManager; +use Gedmo\Sluggable\Sluggable; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\DateTimeTypes\ArticleDate; +use Gedmo\Tests\Sluggable\Fixture\DateTimeTypes\ArticleDateImmutable; +use Gedmo\Tests\Sluggable\Fixture\DateTimeTypes\ArticleDateTime; +use Gedmo\Tests\Sluggable\Fixture\DateTimeTypes\ArticleDateTimeImmutable; +use Gedmo\Tests\Sluggable\Fixture\DateTimeTypes\ArticleDateTimeTz; +use Gedmo\Tests\Sluggable\Fixture\DateTimeTypes\ArticleDateTimeTzImmutable; +use Gedmo\Tests\Tool\BaseTestCaseORM; + +/** + * These are tests for sluggable behavior + * + * @author Gediminas Morkevicius + */ +final class SluggableDateTimeTypesTest extends BaseTestCaseORM +{ + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $evm->addEventSubscriber(new SluggableListener()); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testShouldBuildSlugWithAllDateTimeTypes(): void + { + $articleDate = new ArticleDate(); + $articleDate->setTitle('the title'); + $articleDate->setCreatedAt(new \DateTime('2022-04-01')); + + $this->em->persist($articleDate); + $this->em->flush(); + $this->em->clear(); + static::assertSame('the-title-2022-04-01', $articleDate->getSlug(), 'with date'); + + $articleDateImmutable = new ArticleDateImmutable(); + $articleDateImmutable->setTitle('the title'); + $articleDateImmutable->setCreatedAt(new \DateTimeImmutable('2022-04-01')); + + $this->em->persist($articleDateImmutable); + $this->em->flush(); + $this->em->clear(); + static::assertSame('the-title-2022-04-01', $articleDateImmutable->getSlug(), 'with date_immutable'); + + $articleDateTime = new ArticleDateTime(); + $articleDateTime->setTitle('the title'); + $articleDateTime->setCreatedAt(new \DateTime('2022-04-01')); + + $this->em->persist($articleDateTime); + $this->em->flush(); + $this->em->clear(); + static::assertSame('the-title-2022-04-01', $articleDateTime->getSlug(), 'with datetime'); + + $articleDateTimeImmutable = new ArticleDateTimeImmutable(); + $articleDateTimeImmutable->setTitle('the title'); + $articleDateTimeImmutable->setCreatedAt(new \DateTimeImmutable('2022-04-01')); + + $this->em->persist($articleDateTimeImmutable); + $this->em->flush(); + $this->em->clear(); + static::assertSame('the-title-2022-04-01', $articleDateTimeImmutable->getSlug(), 'with datetime_immutable'); + + $articleDateTimeTz = new ArticleDateTimeTz(); + $articleDateTimeTz->setTitle('the title'); + $articleDateTimeTz->setCreatedAt(new \DateTime('2022-04-01')); + + $this->em->persist($articleDateTimeTz); + $this->em->flush(); + $this->em->clear(); + static::assertSame('the-title-2022-04-01', $articleDateTimeTz->getSlug(), 'with datetimetz'); + + $articleDateTimeTzImmutable = new ArticleDateTimeTzImmutable(); + $articleDateTimeTzImmutable->setTitle('the title'); + $articleDateTimeTzImmutable->setCreatedAt(new \DateTimeImmutable('2022-04-01')); + + $this->em->persist($articleDateTimeTzImmutable); + $this->em->flush(); + $this->em->clear(); + static::assertSame('the-title-2022-04-01', $articleDateTimeTzImmutable->getSlug(), 'with datetimetz_immutable'); + } + + protected function getUsedEntityFixtures(): array + { + return [ + ArticleDate::class, + ArticleDateImmutable::class, + ArticleDateTime::class, + ArticleDateTimeImmutable::class, + ArticleDateTimeTz::class, + ArticleDateTimeTzImmutable::class, + ]; + } +} diff --git a/tests/Gedmo/Sluggable/SluggableDocumentTest.php b/tests/Gedmo/Sluggable/SluggableDocumentTest.php index 0b0db3e65a..18c29739ba 100644 --- a/tests/Gedmo/Sluggable/SluggableDocumentTest.php +++ b/tests/Gedmo/Sluggable/SluggableDocumentTest.php @@ -1,39 +1,45 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Sluggable\Fixture\Document\Article; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Document\Article; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SluggableDocumentTest extends BaseTestCaseMongoODM +final class SluggableDocumentTest extends BaseTestCaseMongoODM { - const ARTICLE = 'Sluggable\\Fixture\\Document\\Article'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); $this->populate(); } - public function testSlugGeneration() + public function testSlugGeneration(): void { // test insert - $repo = $this->dm->getRepository(self::ARTICLE); - $article = $repo->findOneByTitle('My Title'); + $repo = $this->dm->getRepository(Article::class); + $article = $repo->findOneBy(['title' => 'My Title']); - $this->assertEquals('my-title-the-code', $article->getSlug()); + static::assertSame('my-title-the-code', $article->getSlug()); // test update $article->setTitle('New Title'); @@ -42,13 +48,13 @@ public function testSlugGeneration() $this->dm->flush(); $this->dm->clear(); - $article = $repo->findOneByTitle('New Title'); - $this->assertEquals('new-title-the-code', $article->getSlug()); + $article = $repo->findOneBy(['title' => 'New Title']); + static::assertSame('new-title-the-code', $article->getSlug()); } - public function testUniqueSlugGeneration() + public function testUniqueSlugGeneration(): void { - for ($i = 0; $i < 12; $i++) { + for ($i = 0; $i < 12; ++$i) { $article = new Article(); $article->setTitle('My Title'); $article->setCode('The Code'); @@ -56,11 +62,11 @@ public function testUniqueSlugGeneration() $this->dm->persist($article); $this->dm->flush(); $this->dm->clear(); - $this->assertEquals('my-title-the-code-'.($i + 1), $article->getSlug()); + static::assertSame('my-title-the-code-'.($i + 1), $article->getSlug()); } } - public function testGithubIssue57() + public function testGithubIssue57(): void { // slug matched by prefix $article = new Article(); @@ -74,10 +80,10 @@ public function testGithubIssue57() $this->dm->persist($article2); $this->dm->flush(); - $this->assertEquals('my-s', $article2->getSlug()); + static::assertSame('my-s', $article2->getSlug()); } - private function populate() + private function populate(): void { $art0 = new Article(); $art0->setTitle('My Title'); diff --git a/tests/Gedmo/Sluggable/SluggableEmbeddableTest.php b/tests/Gedmo/Sluggable/SluggableEmbeddableTest.php new file mode 100644 index 0000000000..5605fb5b6f --- /dev/null +++ b/tests/Gedmo/Sluggable/SluggableEmbeddableTest.php @@ -0,0 +1,55 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Doctrine\Common\EventManager; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Embeddable\Address; +use Gedmo\Tests\Sluggable\Fixture\Embeddable\User; +use Gedmo\Tests\Tool\BaseTestCaseORM; + +final class SluggableEmbeddableTest extends BaseTestCaseORM +{ + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $evm->addEventSubscriber(new SluggableListener()); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testShouldHandleSlugWithEmbeddable(): void + { + $address = new Address(); + $address->setStreet('street'); + $address->setCity('city'); + $address->setPostalCode('postal code'); + $address->setCountry('country'); + + $user = new User(); + $user->setUsername('username'); + $user->setAddress($address); + + $this->em->persist($user); + $this->em->flush(); + $this->em->clear(); + + static::assertSame('username-city-country', $user->getSlug()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + User::class, + ]; + } +} diff --git a/tests/Gedmo/Sluggable/SluggableFltersTest.php b/tests/Gedmo/Sluggable/SluggableFltersTest.php index dcd1490ef7..d94e718a89 100644 --- a/tests/Gedmo/Sluggable/SluggableFltersTest.php +++ b/tests/Gedmo/Sluggable/SluggableFltersTest.php @@ -1,26 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Article; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter; +use Gedmo\Tests\Sluggable\Fixture\Article; +use Gedmo\Tests\Sluggable\Fixture\Doctrine\FakeFilter; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Florian Vilpoix - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SluggableFltersTest extends BaseTestCaseORM +final class SluggableFltersTest extends BaseTestCaseORM { - const TARGET = 'Sluggable\\Fixture\\Article'; + private const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable'; + private const FAKE_FILTER_NAME = 'fake-filter'; - const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable'; - const FAKE_FILTER_NAME = 'fake-filter'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -30,27 +38,17 @@ protected function setUp() $sluggableListener->addManagedFilter(self::FAKE_FILTER_NAME, true); $evm->addEventSubscriber($sluggableListener); - $config = $this->getMockAnnotatedConfig(); - $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, 'Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter'); - $config->addFilter(self::FAKE_FILTER_NAME, 'Sluggable\Fixture\Doctrine\FakeFilter'); + $config = $this->getDefaultConfiguration(); + $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, SoftDeleteableFilter::class); + $config->addFilter(self::FAKE_FILTER_NAME, FakeFilter::class); - $this->em = $this->getMockSqliteEntityManager($evm, $config); + $this->em = $this->getDefaultMockSqliteEntityManager($evm, $config); $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); $this->em->getFilters()->enable(self::FAKE_FILTER_NAME); } - protected function getUsedEntityFixtures() - { - return array( - self::TARGET, - ); - } - - /** - * @test - */ - public function shouldSuccessWhenManagedFilterHasAlreadyBeenDisabled() + public function testShouldSuccessWhenManagedFilterHasAlreadyBeenDisabled(): void { // disable one managed doctrine filter $this->em->getFilters()->disable(self::FAKE_FILTER_NAME); @@ -62,6 +60,13 @@ public function shouldSuccessWhenManagedFilterHasAlreadyBeenDisabled() $this->em->persist($slug); $this->em->flush(); - $this->assertEquals('my-title-my-code', $slug->getSlug()); + static::assertSame('my-title-my-code', $slug->getSlug()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/SluggableIdentifierTest.php b/tests/Gedmo/Sluggable/SluggableIdentifierTest.php index 7e5a000c9f..ce7c53c12d 100644 --- a/tests/Gedmo/Sluggable/SluggableIdentifierTest.php +++ b/tests/Gedmo/Sluggable/SluggableIdentifierTest.php @@ -1,49 +1,49 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Identifier; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Identifier; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SluggableIdentifierTest extends BaseTestCaseORM +final class SluggableIdentifierTest extends BaseTestCaseORM { - const TARGET = 'Sluggable\\Fixture\\Identifier'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldBePossibleToSlugIdentifiers() + public function testShouldBePossibleToSlugIdentifiers(): void { $sport = new Identifier(); $sport->setTitle('Sport'); $this->em->persist($sport); $this->em->flush(); - $this->assertEquals('sport', $sport->getId()); + static::assertSame('sport', $sport->getId()); } - /** - * @test - */ - public function shouldPersistMultipleNonConflictingIdentifierSlugs() + public function testShouldPersistMultipleNonConflictingIdentifierSlugs(): void { $sport = new Identifier(); $sport->setTitle('Sport'); @@ -54,14 +54,14 @@ public function shouldPersistMultipleNonConflictingIdentifierSlugs() $this->em->persist($sport2); $this->em->flush(); - $this->assertEquals('sport', $sport->getId()); - $this->assertEquals('sport_1', $sport2->getId()); + static::assertSame('sport', $sport->getId()); + static::assertSame('sport_1', $sport2->getId()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TARGET, - ); + return [ + Identifier::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/SluggablePositionTest.php b/tests/Gedmo/Sluggable/SluggablePositionTest.php index 9a5d417e1d..bf4df9ca3c 100644 --- a/tests/Gedmo/Sluggable/SluggablePositionTest.php +++ b/tests/Gedmo/Sluggable/SluggablePositionTest.php @@ -1,53 +1,59 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Position; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Position; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SluggablePositionTest extends BaseTestCaseORM +final class SluggablePositionTest extends BaseTestCaseORM { - const POSITION = 'Sluggable\\Fixture\\Position'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testPositionedSlugOrder() + public function testPositionedSlugOrder(): void { - $meta = $this->em->getClassMetadata(self::POSITION); - $repo = $this->em->getRepository(self::POSITION); + $meta = $this->em->getClassMetadata(Position::class); + $repo = $this->em->getRepository(Position::class); $object = $repo->find(1); $slug = $meta->getReflectionProperty('slug')->getValue($object); - $this->assertEquals('code-other-title-prop', $slug); + static::assertSame('code-other-title-prop', $slug); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::POSITION, - ); + return [ + Position::class, + ]; } - private function populate() + private function populate(): void { - $meta = $this->em->getClassMetadata(self::POSITION); + $meta = $this->em->getClassMetadata(Position::class); $object = new Position(); $meta->getReflectionProperty('title')->setValue($object, 'title'); $meta->getReflectionProperty('prop')->setValue($object, 'prop'); diff --git a/tests/Gedmo/Sluggable/SluggablePrefixSuffixTest.php b/tests/Gedmo/Sluggable/SluggablePrefixSuffixTest.php index eed689a778..0a00bec890 100644 --- a/tests/Gedmo/Sluggable/SluggablePrefixSuffixTest.php +++ b/tests/Gedmo/Sluggable/SluggablePrefixSuffixTest.php @@ -1,23 +1,28 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; use Doctrine\Common\EventManager; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Prefix; +use Gedmo\Tests\Sluggable\Fixture\PrefixWithTreeHandler; +use Gedmo\Tests\Sluggable\Fixture\Suffix; +use Gedmo\Tests\Sluggable\Fixture\SuffixWithTreeHandler; +use Gedmo\Tests\Tool\BaseTestCaseORM; use Gedmo\Tree\TreeListener; -use Sluggable\Fixture\Prefix; -use Sluggable\Fixture\PrefixWithTreeHandler; -use Sluggable\Fixture\Suffix; -use Sluggable\Fixture\SuffixWithTreeHandler; -use Tool\BaseTestCaseORM; -class SluggablePrefixSuffixTest extends BaseTestCaseORM +final class SluggablePrefixSuffixTest extends BaseTestCaseORM { - const PREFIX = 'Sluggable\\Fixture\\Prefix'; - const SUFFIX = 'Sluggable\\Fixture\\Suffix'; - const SUFFIX_TREE = 'Sluggable\\Fixture\\SuffixWithTreeHandler'; - const PREFIX_TREE = 'Sluggable\\Fixture\\PrefixWithTreeHandler'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -25,39 +30,30 @@ protected function setUp() $evm->addEventSubscriber(new SluggableListener()); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function testPrefix() + public function testPrefix(): void { $foo = new Prefix(); $foo->setTitle('Foo'); $this->em->persist($foo); $this->em->flush(); - $this->assertEquals('test-foo', $foo->getSlug()); + static::assertSame('test-foo', $foo->getSlug()); } - /** - * @test - */ - public function testSuffix() + public function testSuffix(): void { $foo = new Suffix(); $foo->setTitle('Foo'); $this->em->persist($foo); $this->em->flush(); - $this->assertEquals('foo.test', $foo->getSlug()); + static::assertSame('foo.test', $foo->getSlug()); } - /** - * @test - */ - public function testNoDuplicateSuffixes() + public function testNoDuplicateSuffixes(): void { $foo = new SuffixWithTreeHandler(); $foo->setTitle('Foo'); @@ -75,13 +71,10 @@ public function testNoDuplicateSuffixes() $this->em->persist($baz); $this->em->flush(); - $this->assertEquals('foo.test/bar.test/baz.test', $baz->getSlug()); + static::assertSame('foo.test/bar.test/baz.test', $baz->getSlug()); } - /** - * @test - */ - public function testNoDuplicatePrefixes() + public function testNoDuplicatePrefixes(): void { $foo = new PrefixWithTreeHandler(); $foo->setTitle('Foo'); @@ -99,21 +92,19 @@ public function testNoDuplicatePrefixes() $this->em->persist($baz); $this->em->flush(); - $this->assertEquals('test.foo/test.bar/test.baz', $baz->getSlug()); + static::assertSame('test.foo/test.bar/test.baz', $baz->getSlug()); } /** * Get a list of used fixture classes - * - * @return array */ - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::SUFFIX, - self::PREFIX, - self::SUFFIX_TREE, - self::PREFIX_TREE, - ); + return [ + Suffix::class, + Prefix::class, + SuffixWithTreeHandler::class, + PrefixWithTreeHandler::class, + ]; } } diff --git a/tests/Gedmo/Sluggable/SluggableTest.php b/tests/Gedmo/Sluggable/SluggableTest.php index c9b231898c..41cc07464b 100644 --- a/tests/Gedmo/Sluggable/SluggableTest.php +++ b/tests/Gedmo/Sluggable/SluggableTest.php @@ -1,51 +1,57 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Article; +use Doctrine\DBAL\DriverManager; +use Doctrine\ORM\EntityManager; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Sluggable\Sluggable; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Article; +use Gedmo\Tests\Sluggable\Fixture\ArticleWithoutFields; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SluggableTest extends BaseTestCaseORM +final class SluggableTest extends BaseTestCaseORM { - const ARTICLE = 'Sluggable\\Fixture\\Article'; - private $articleId; + private ?int $articleId = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - /** - * @test - */ - public function shouldInsertNewSlug() + public function testShouldInsertNewSlug(): void { - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(Article::class, $this->articleId); - $this->assertTrue($article instanceof Sluggable); - $this->assertEquals($article->getSlug(), 'the-title-my-code'); + static::assertInstanceOf(Sluggable::class, $article); + static::assertSame('the-title-my-code', $article->getSlug()); } - /** - * @test - */ - public function shouldBuildUniqueSlug() + public function testShouldBuildUniqueSlug(): void { - for ($i = 0; $i < 12; $i++) { + for ($i = 0; $i < 12; ++$i) { $article = new Article(); $article->setTitle('the title'); $article->setCode('my code'); @@ -53,14 +59,11 @@ public function shouldBuildUniqueSlug() $this->em->persist($article); $this->em->flush(); $this->em->clear(); - $this->assertEquals($article->getSlug(), 'the-title-my-code-'.($i + 1)); + static::assertSame($article->getSlug(), 'the-title-my-code-'.($i + 1)); } } - /** - * @test - */ - public function shouldHandleUniqueSlugLimitedLength() + public function testShouldHandleUniqueSlugLimitedLength(): void { $long = 'the title the title the title the title the title the title the title'; $article = new Article(); @@ -70,7 +73,9 @@ public function shouldHandleUniqueSlugLimitedLength() $this->em->persist($article); $this->em->flush(); $this->em->clear(); - for ($i = 0; $i < 12; $i++) { + for ($i = 1; $i <= 12; ++$i) { + $uniqueSuffix = (string) $i; + $article = new Article(); $article->setTitle($long); $article->setCode('my code'); @@ -80,16 +85,14 @@ public function shouldHandleUniqueSlugLimitedLength() $this->em->clear(); $shorten = $article->getSlug(); - $this->assertEquals(64, strlen($shorten)); + static::assertSame(64, strlen($shorten)); $expected = 'the-title-the-title-the-title-the-title-the-title-the-title-the-'; - $expected = substr($expected, 0, 64 - (strlen($i+1) + 1)).'-'.($i+1); - $this->assertEquals($shorten, $expected); + $expected = substr($expected, 0, 64 - (strlen($uniqueSuffix) + 1)).'-'.$uniqueSuffix; + static::assertSame($shorten, $expected); } } - /** - * @test - */ - public function doubleDelimiterShouldBeRemoved() + + public function testDoubleDelimiterShouldBeRemoved(): void { $long = 'Sample long title which should be correctly slugged blablabla'; $article = new Article(); @@ -103,15 +106,12 @@ public function doubleDelimiterShouldBeRemoved() $this->em->persist($article2); $this->em->flush(); $this->em->clear(); - $this->assertEquals("sample-long-title-which-should-be-correctly-slugged-blablabla-my", $article->getSlug()); + static::assertSame('sample-long-title-which-should-be-correctly-slugged-blablabla-my', $article->getSlug()); // OLD IMPLEMENTATION PRODUCE SLUG sample-long-title-which-should-be-correctly-slugged-blablabla--1 - $this->assertEquals("sample-long-title-which-should-be-correctly-slugged-blablabla-1", $article2->getSlug()); + static::assertSame('sample-long-title-which-should-be-correctly-slugged-blablabla-1', $article2->getSlug()); } - /** - * @test - */ - public function shouldHandleNumbersInSlug() + public function testShouldHandleNumbersInSlug(): void { $article = new Article(); $article->setTitle('the title'); @@ -119,7 +119,7 @@ public function shouldHandleNumbersInSlug() $this->em->persist($article); $this->em->flush(); - for ($i = 0; $i < 12; $i++) { + for ($i = 0; $i < 12; ++$i) { $article = new Article(); $article->setTitle('the title'); $article->setCode('my code 123'); @@ -127,42 +127,33 @@ public function shouldHandleNumbersInSlug() $this->em->persist($article); $this->em->flush(); $this->em->clear(); - $this->assertEquals($article->getSlug(), 'the-title-my-code-123-'.($i + 1)); + static::assertSame($article->getSlug(), 'the-title-my-code-123-'.($i + 1)); } } - /** - * @test - */ - public function shouldUpdateSlug() + public function testShouldUpdateSlug(): void { - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(Article::class, $this->articleId); $article->setTitle('the title updated'); $this->em->persist($article); $this->em->flush(); - $this->assertSame('the-title-updated-my-code', $article->getSlug()); + static::assertSame('the-title-updated-my-code', $article->getSlug()); } - /** - * @test - */ - public function shouldBeAbleToForceRegenerationOfSlug() + public function testShouldBeAbleToForceRegenerationOfSlug(): void { - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(Article::class, $this->articleId); $article->setSlug(null); $this->em->persist($article); $this->em->flush(); - $this->assertSame('the-title-my-code', $article->getSlug()); + static::assertSame('the-title-my-code', $article->getSlug()); } - /** - * @test - */ - public function shouldBeAbleToForceTheSlug() + public function testShouldBeAbleToForceTheSlug(): void { - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(Article::class, $this->articleId); $article->setSlug('my-forced-slug'); $this->em->persist($article); @@ -173,14 +164,11 @@ public function shouldBeAbleToForceTheSlug() $this->em->persist($new); $this->em->flush(); - $this->assertSame('my-forced-slug', $article->getSlug()); - $this->assertSame('forced', $new->getSlug()); + static::assertSame('my-forced-slug', $article->getSlug()); + static::assertSame('forced', $new->getSlug()); } - /** - * @test - */ - public function shouldSolveGithubIssue45() + public function testShouldSolveGithubIssue45(): void { // persist new records with same slug $article = new Article(); @@ -194,14 +182,11 @@ public function shouldSolveGithubIssue45() $this->em->persist($article2); $this->em->flush(); - $this->assertEquals('test-code', $article->getSlug()); - $this->assertEquals('test-code-1', $article2->getSlug()); + static::assertSame('test-code', $article->getSlug()); + static::assertSame('test-code-1', $article2->getSlug()); } - /** - * @test - */ - public function shouldSolveGithubIssue57() + public function testShouldSolveGithubIssue57(): void { // slug matched by prefix $article = new Article(); @@ -215,27 +200,24 @@ public function shouldSolveGithubIssue57() $this->em->persist($article2); $this->em->flush(); - $this->assertEquals('my-s', $article2->getSlug()); + static::assertSame('my-s', $article2->getSlug()); } - /** - * @test - */ - public function shouldAllowForcingEmptySlugAndRegenerateIfNullIssue807() + public function testShouldAllowForcingEmptySlugAndRegenerateIfNullIssue807(): void { - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(Article::class, $this->articleId); $article->setSlug(''); $this->em->persist($article); $this->em->flush(); - $this->assertSame('', $article->getSlug()); + static::assertSame('', $article->getSlug()); $article->setSlug(null); $this->em->persist($article); $this->em->flush(); - $this->assertSame('the-title-my-code', $article->getSlug()); + static::assertSame('the-title-my-code', $article->getSlug()); $same = new Article(); $same->setTitle('any'); @@ -244,17 +226,45 @@ public function shouldAllowForcingEmptySlugAndRegenerateIfNullIssue807() $this->em->persist($same); $this->em->flush(); - $this->assertSame('the-title-my-code-1', $same->getSlug()); + static::assertSame('the-title-my-code-1', $same->getSlug()); } - protected function getUsedEntityFixtures() + public function testRequiredFields(): void { - return array( - self::ARTICLE, + $eventManager = new EventManager(); + $eventManager->addEventSubscriber(new SluggableListener()); + + $config = $this->getDefaultConfiguration(); + + $em = new EntityManager( + DriverManager::getConnection( + [ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ], + $config + ), + $config, + $eventManager ); + + $this->expectException(InvalidMappingException::class); + $this->expectExceptionMessage(\sprintf( + 'Slug must contain at least one field for slug generation in class - %s', + ArticleWithoutFields::class + )); + + $em->getClassMetadata(ArticleWithoutFields::class); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + ]; } - private function populate() + private function populate(): void { $article = new Article(); $article->setTitle('the title'); diff --git a/tests/Gedmo/Sluggable/TranslatableManySlugTest.php b/tests/Gedmo/Sluggable/TranslatableManySlugTest.php index b56ffd8bb6..2a3f752321 100644 --- a/tests/Gedmo/Sluggable/TranslatableManySlugTest.php +++ b/tests/Gedmo/Sluggable/TranslatableManySlugTest.php @@ -1,30 +1,37 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Gedmo\Translatable\Translatable; +use Gedmo\Sluggable\Sluggable; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\TransArticleManySlug; +use Gedmo\Tests\Tool\BaseTestCaseORM; use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Translatable; use Gedmo\Translatable\TranslatableListener; -use Sluggable\Fixture\TransArticleManySlug; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableManySlugTest extends BaseTestCaseORM +final class TranslatableManySlugTest extends BaseTestCaseORM { - private $articleId; - private $translatableListener; + private ?int $articleId = null; - const ARTICLE = 'Sluggable\\Fixture\\TransArticleManySlug'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -34,22 +41,22 @@ protected function setUp() $evm->addEventSubscriber(new SluggableListener()); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testSlugAndTranslation() + public function testSlugAndTranslation(): void { - $article = $this->em->find(self::ARTICLE, $this->articleId); - $this->assertTrue($article instanceof Translatable && $article instanceof Sluggable); - $this->assertEquals('the-title-my-code', $article->getSlug()); - $this->assertEquals('the-unique-title', $article->getUniqueSlug()); - $repo = $this->em->getRepository(self::TRANSLATION); + $article = $this->em->find(TransArticleManySlug::class, $this->articleId); + static::assertTrue($article instanceof Translatable && $article instanceof Sluggable); + static::assertSame('the-title-my-code', $article->getSlug()); + static::assertSame('the-unique-title', $article->getUniqueSlug()); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($article); - $this->assertCount(0, $translations); + static::assertCount(0, $translations); - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(TransArticleManySlug::class, $this->articleId); $article->setTranslatableLocale('de_DE'); $article->setCode('code in de'); $article->setTitle('title in de'); @@ -58,19 +65,19 @@ public function testSlugAndTranslation() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($article); - $this->assertCount(1, $translations); - $this->assertArrayHasKey('de_DE', $translations); - $this->assertCount(3, $translations['de_DE']); + static::assertCount(1, $translations); + static::assertArrayHasKey('de_DE', $translations); + static::assertCount(3, $translations['de_DE']); - $this->assertEquals('title in de', $translations['de_DE']['title']); + static::assertSame('title in de', $translations['de_DE']['title']); - $this->assertArrayHasKey('slug', $translations['de_DE']); - $this->assertEquals('title-in-de-code-in-de', $translations['de_DE']['slug']); + static::assertArrayHasKey('slug', $translations['de_DE']); + static::assertSame('title-in-de-code-in-de', $translations['de_DE']['slug']); } - public function testUniqueness() + public function testUniqueness(): void { $a0 = new TransArticleManySlug(); $a0->setTitle('the title'); @@ -87,22 +94,22 @@ public function testUniqueness() $this->em->persist($a1); $this->em->flush(); - $this->assertEquals('title', $a0->getUniqueSlug()); - $this->assertEquals('title-1', $a1->getUniqueSlug()); - // if its translated maybe should be different - $this->assertEquals('the-title-my-code-1', $a0->getSlug()); - $this->assertEquals('the-title-my-code-2', $a1->getSlug()); + static::assertSame('title', $a0->getUniqueSlug()); + static::assertSame('title-1', $a1->getUniqueSlug()); + // if its translated maybe should be different + static::assertSame('the-title-my-code-1', $a0->getSlug()); + static::assertSame('the-title-my-code-2', $a1->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::TRANSLATION, - ); + return [ + TransArticleManySlug::class, + Translation::class, + ]; } - private function populate() + private function populate(): void { $article = new TransArticleManySlug(); $article->setTitle('the title'); diff --git a/tests/Gedmo/Sluggable/TranslatableSlugTest.php b/tests/Gedmo/Sluggable/TranslatableSlugTest.php index fb9ceaf6c3..a9887e4c92 100644 --- a/tests/Gedmo/Sluggable/TranslatableSlugTest.php +++ b/tests/Gedmo/Sluggable/TranslatableSlugTest.php @@ -1,34 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Gedmo\Translatable\Translatable; +use Gedmo\Sluggable\Sluggable; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Comment; +use Gedmo\Tests\Sluggable\Fixture\Page; +use Gedmo\Tests\Sluggable\Fixture\TranslatableArticle; +use Gedmo\Tests\Tool\BaseTestCaseORM; use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Translatable; use Gedmo\Translatable\TranslatableListener; -use Sluggable\Fixture\TranslatableArticle; -use Sluggable\Fixture\Comment; -use Sluggable\Fixture\Page; /** * These are tests for Sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableSlugTest extends BaseTestCaseORM +final class TranslatableSlugTest extends BaseTestCaseORM { - private $articleId; - private $translatableListener; + private ?int $articleId = null; - const ARTICLE = 'Sluggable\\Fixture\\TranslatableArticle'; - const COMMENT = 'Sluggable\\Fixture\\Comment'; - const PAGE = 'Sluggable\\Fixture\\Page'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -38,21 +43,21 @@ protected function setUp() $evm->addEventSubscriber(new SluggableListener()); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testSlugAndTranslation() + public function testSlugAndTranslation(): void { - $article = $this->em->find(self::ARTICLE, $this->articleId); - $this->assertTrue($article instanceof Translatable && $article instanceof Sluggable); - $this->assertEquals('the-title-my-code', $article->getSlug()); - $repo = $this->em->getRepository(self::TRANSLATION); + $article = $this->em->find(TranslatableArticle::class, $this->articleId); + static::assertTrue($article instanceof Translatable && $article instanceof Sluggable); + static::assertSame('the-title-my-code', $article->getSlug()); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($article); - $this->assertCount(0, $translations); + static::assertCount(0, $translations); - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(TranslatableArticle::class, $this->articleId); $article->setTranslatableLocale('de_DE'); $article->setCode('code in de'); $article->setTitle('title in de'); @@ -61,23 +66,23 @@ public function testSlugAndTranslation() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($article); - $this->assertCount(1, $translations); - $this->assertArrayHasKey('de_DE', $translations); - $this->assertCount(3, $translations['de_DE']); + static::assertCount(1, $translations); + static::assertArrayHasKey('de_DE', $translations); + static::assertCount(3, $translations['de_DE']); - $this->assertArrayHasKey('code', $translations['de_DE']); - $this->assertEquals('code in de', $translations['de_DE']['code']); + static::assertArrayHasKey('code', $translations['de_DE']); + static::assertSame('code in de', $translations['de_DE']['code']); - $this->assertArrayHasKey('title', $translations['de_DE']); - $this->assertEquals('title in de', $translations['de_DE']['title']); + static::assertArrayHasKey('title', $translations['de_DE']); + static::assertSame('title in de', $translations['de_DE']['title']); - $this->assertArrayHasKey('slug', $translations['de_DE']); - $this->assertEquals('title-in-de-code-in-de', $translations['de_DE']['slug']); + static::assertArrayHasKey('slug', $translations['de_DE']); + static::assertSame('title-in-de-code-in-de', $translations['de_DE']['slug']); } - public function testConcurrentChanges() + public function testConcurrentChanges(): void { $page = new Page(); $page->setContent('cont test'); @@ -85,7 +90,7 @@ public function testConcurrentChanges() $a0Page = new Page(); $a0Page->setContent('bi vv'); - $article0 = $this->em->find(self::ARTICLE, $this->articleId); + $article0 = $this->em->find(TranslatableArticle::class, $this->articleId); $article0->setCode('cell'); $article0->setTitle('xx gg'); $a0Page->addArticle($article0); @@ -124,20 +129,20 @@ public function testConcurrentChanges() $this->em->flush(); $this->em->clear(); - $this->assertEquals($page->getSlug(), 'Cont_Test'); + static::assertSame('Cont_Test', $page->getSlug()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::COMMENT, - self::PAGE, - self::TRANSLATION, - ); + return [ + TranslatableArticle::class, + Comment::class, + Page::class, + Translation::class, + ]; } - private function populate() + private function populate(): void { $article = new TranslatableArticle(); $article->setTitle('the title'); diff --git a/tests/Gedmo/Sluggable/TransliterationTest.php b/tests/Gedmo/Sluggable/TransliterationTest.php index 9fe33232a9..45c8fe9fd0 100644 --- a/tests/Gedmo/Sluggable/TransliterationTest.php +++ b/tests/Gedmo/Sluggable/TransliterationTest.php @@ -1,51 +1,64 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sluggable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Sluggable\Fixture\Article; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Sluggable\Fixture\Article; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TransliterationTest extends BaseTestCaseORM +final class TransliterationTest extends BaseTestCaseORM { - const ARTICLE = 'Sluggable\\Fixture\\Article'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SluggableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testInsertedNewSlug() + public function testInsertedNewSlug(): void { - $repo = $this->em->getRepository(self::ARTICLE); + $repo = $this->em->getRepository(Article::class); + + $lithuanian = $repo->findOneBy(['code' => 'lt']); + static::assertSame('transliteration-test-usage-uz-lt', $lithuanian->getSlug()); - $lithuanian = $repo->findOneByCode('lt'); - $this->assertEquals('transliteration-test-usage-uz-lt', $lithuanian->getSlug()); + $bulgarian = $repo->findOneBy(['code' => 'bg']); + static::assertSame('tova-e-testovo-zaglavie-bg', $bulgarian->getSlug()); - $bulgarian = $repo->findOneByCode('bg'); - $this->assertEquals('tova-ie-tiestovo-zaghlaviie-bg', $bulgarian->getSlug()); + $russian = $repo->findOneBy(['code' => 'ru']); + static::assertSame('eto-testovyj-zagolovok-ru', $russian->getSlug()); - $russian = $repo->findOneByCode('ru'); - $this->assertEquals('eto-tiestovyi-zagholovok-ru', $russian->getSlug()); + $german = $repo->findOneBy(['code' => 'de']); + static::assertSame('fuhren-aktivitaten-haglofs-de', $german->getSlug()); + } - $german = $repo->findOneByCode('de'); - $this->assertEquals('fuhren-aktivitaten-haglofs-de', $german->getSlug()); + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + ]; } - private function populate() + private function populate(): void { $lithuanian = new Article(); $lithuanian->setTitle('trฤ…nslฤฏteration tฤ—st ลณsฤ…ge ลซลพ'); @@ -70,11 +83,4 @@ private function populate() $this->em->flush(); $this->em->clear(); } - - protected function getUsedEntityFixtures() - { - return array( - self::ARTICLE, - ); - } } diff --git a/tests/Gedmo/SoftDeleteable/CarbonTest.php b/tests/Gedmo/SoftDeleteable/CarbonTest.php new file mode 100644 index 0000000000..3111abfd61 --- /dev/null +++ b/tests/Gedmo/SoftDeleteable/CarbonTest.php @@ -0,0 +1,102 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable; + +use Carbon\Carbon; +use Carbon\Doctrine\DateTimeType; +use Doctrine\Common\EventManager; +use Doctrine\DBAL\Types\Type as DoctrineType; +use Doctrine\DBAL\Types\Types; +use Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter; +use Gedmo\SoftDeleteable\SoftDeleteableListener; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Article; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Comment; +use Gedmo\Tests\Tool\BaseTestCaseORM; + +final class CarbonTest extends BaseTestCaseORM +{ + private const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable'; + + private SoftDeleteableListener $softDeleteableListener; + + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $this->softDeleteableListener = new SoftDeleteableListener(); + $evm->addEventSubscriber($this->softDeleteableListener); + $config = $this->getDefaultConfiguration(); + $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, SoftDeleteableFilter::class); + $this->em = $this->getDefaultMockSqliteEntityManager($evm, $config); + $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); + + DoctrineType::overrideType(Types::DATETIME_MUTABLE, DateTimeType::class); + } + + protected function tearDown(): void + { + parent::tearDown(); + + DoctrineType::overrideType(Types::DATETIME_MUTABLE, \Doctrine\DBAL\Types\DateTimeType::class); + } + + public function testSoftDeleteable(): void + { + $repo = $this->em->getRepository(Article::class); + $commentRepo = $this->em->getRepository(Comment::class); + + $comment = new Comment(); + $commentField = 'comment'; + $commentValue = 'Comment 1'; + $comment->setComment($commentValue); + $art0 = new Article(); + $field = 'title'; + $value = 'Title 1'; + $art0->setTitle($value); + $art0->addComment($comment); + + $this->em->persist($art0); + $this->em->flush(); + + $art = $repo->findOneBy([$field => $value]); + + static::assertNull($art->getDeletedAt()); + static::assertNull($comment->getDeletedAt()); + + $this->em->remove($art); + $this->em->flush(); + + $art = $repo->findOneBy([$field => $value]); + static::assertNull($art); + $comment = $commentRepo->findOneBy([$commentField => $commentValue]); + static::assertNull($comment); + + // Now we deactivate the filter so we test if the entity appears in the result + $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); + + $art = $repo->findOneBy([$field => $value]); + static::assertIsObject($art); + static::assertIsObject($art->getDeletedAt()); + static::assertInstanceOf(Carbon::class, $art->getDeletedAt()); + $comment = $commentRepo->findOneBy([$commentField => $commentValue]); + static::assertIsObject($comment); + static::assertIsObject($comment->getDeletedAt()); + static::assertInstanceOf(Carbon::class, $comment->getDeletedAt()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + Comment::class, + ]; + } +} diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Document/User.php b/tests/Gedmo/SoftDeleteable/Fixture/Document/User.php index 27197f7b84..c17155b1e0 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Document/User.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Document/User.php @@ -1,60 +1,73 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Document; -use Gedmo\Mapping\Annotation as Gedmo; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="users") + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ODM\Document(collection: 'users')] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class User { - /** @ODM\Id */ - private $id; - - /** @ODM\String */ - private $username; - - /** @ODM\Date */ - protected $deletedAt; - /** - * Sets deletedAt. + * @var \DateTime|null * - * @param Datetime $deletedAt + * @ODM\Field(type="date") + */ + #[ODM\Field(type: Type::DATE)] + protected $deletedAt; + /** + * @var string|null * - * @return $this + * @ODM\Id + */ + #[ODM\Id] + private $id; + + /** + * @ODM\Field(type="string") */ - public function setDeletedAt(\DateTime $deletedAt) + #[ODM\Field(type: Type::STRING)] + private ?string $username = null; + + public function setDeletedAt(\DateTime $deletedAt): self { $this->deletedAt = $deletedAt; return $this; } - /** - * Returns deletedAt. - * - * @return DateTime - */ - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } - public function getId() + public function getId(): ?string { return $this->id; } - public function setUsername($username) + public function setUsername(?string $username): void { $this->username = $username; } - public function getUsername() + public function getUsername(): ?string { return $this->username; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Document/UserTimeAware.php b/tests/Gedmo/SoftDeleteable/Fixture/Document/UserTimeAware.php index 9d8a8af9c1..0816f418a8 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Document/UserTimeAware.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Document/UserTimeAware.php @@ -1,60 +1,73 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Document; -use Gedmo\Mapping\Annotation as Gedmo; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="users") + * * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=true) */ +#[ODM\Document(collection: 'users')] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', timeAware: true)] class UserTimeAware { - /** @ODM\Id */ - private $id; - - /** @ODM\String */ - private $username; - - /** @ODM\Date */ - protected $deletedAt; - /** - * Sets deletedAt. + * @var \DateTime|null * - * @param Datetime $deletedAt + * @ODM\Field(type="date") + */ + #[ODM\Field(type: Type::DATE)] + protected $deletedAt; + /** + * @var string|null * - * @return $this + * @ODM\Id + */ + #[ODM\Id] + private $id; + + /** + * @ODM\Field(type="string") */ - public function setDeletedAt(\DateTime $deletedAt) + #[ODM\Field(type: Type::STRING)] + private ?string $username = null; + + public function setDeletedAt(\DateTime $deletedAt): self { $this->deletedAt = $deletedAt; return $this; } - /** - * Returns deletedAt. - * - * @return DateTime - */ - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } - public function getId() + public function getId(): ?string { return $this->id; } - public function setUsername($username) + public function setUsername(?string $username): void { $this->username = $username; } - public function getUsername() + public function getUsername(): ?string { return $this->username; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Document/UsingTrait.php b/tests/Gedmo/SoftDeleteable/Fixture/Document/UsingTrait.php index 903dc46e00..4ef49a2adb 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Document/UsingTrait.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Document/UsingTrait.php @@ -1,13 +1,20 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Document; use Gedmo\SoftDeleteable\Traits\SoftDeleteableDocument; /** * Class UsingTrait - * - * @package SoftDeleteable\Fixture\Document */ class UsingTrait { diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Address.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Address.php index 1062f3cb35..fdb25f80c8 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Address.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Address.php @@ -1,75 +1,96 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class Address { /** - * @ORM\Column(type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=128) */ - private $street; + #[ORM\Column(length: 128)] + private ?string $street = null; /** * @ORM\Column(type="datetime", nullable=true) */ - private $deletedAt; + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; /** * @ORM\OneToOne(targetEntity="Person", mappedBy="address", cascade={"remove"}) */ - private $owner; + #[ORM\OneToOne(targetEntity: Person::class, mappedBy: 'address', cascade: ['remove'])] + private ?Person $owner = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setStreet($street) + public function setStreet(?string $street): self { $this->street = $street; return $this; } - public function getStreet() + public function getStreet(): ?string { return $this->street; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(?\DateTime $deletedAt): self { $this->deletedAt = $deletedAt; return $this; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } - public function setOwner(Person $owner) + public function setOwner(Person $owner): self { $this->owner = $owner; return $this; } - public function getOwner() + public function getOwner(): ?Person { return $this->owner; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php index 1ad762367b..928bd38757 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php @@ -1,71 +1,119 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; -use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class Article { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string") */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING)] + private ?string $title = null; /** * @ORM\Column(name="deletedAt", type="datetime", nullable=true) */ - private $deletedAt; + #[ORM\Column(name: 'deletedAt', type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Comment", mappedBy="article", cascade={"persist", "remove"}) */ + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article', cascade: ['persist', 'remove'])] private $comments; + /** + * @ORM\ManyToOne(targetEntity="Author", cascade={"persist"}, inversedBy="articles") + */ + #[ORM\ManyToOne(targetEntity: Author::class, cascade: ['persist'], inversedBy: 'articles')] + private ?Author $author = null; + public function __construct() { $this->comments = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(?\DateTime $deletedAt): void { $this->deletedAt = $deletedAt; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } - public function addComment(Comment $comment) + public function addComment(Comment $comment): void { $this->comments[] = $comment; } + + /** + * @return Collection + */ + public function getComments(): Collection + { + return $this->comments; + } + + public function setAuthor(?Author $author): void + { + $this->author = $author; + } + + public function getAuthor(): ?Author + { + return $this->author; + } } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Author.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Author.php new file mode 100644 index 0000000000..935fc60765 --- /dev/null +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Author.php @@ -0,0 +1,106 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity; + +/** + * @ORM\Entity + * + * @Gedmo\SoftDeleteable(fieldName="deletedAt") + */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] +class Author +{ + use SoftDeleteableEntity; + + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(type="string") + */ + #[ORM\Column(type: Types::STRING)] + private ?string $firstname = null; + + /** + * @ORM\Column(type="string") + */ + #[ORM\Column(type: Types::STRING)] + private ?string $lastname = null; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="Article", mappedBy="author", cascade={"persist"}) + */ + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'author', cascade: ['persist'])] + private Collection $articles; + + public function __construct() + { + $this->articles = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function setFirstname(?string $firstname): void + { + $this->firstname = $firstname; + } + + public function getFirstname(): ?string + { + return $this->firstname; + } + + public function setLastname(?string $lastname): void + { + $this->lastname = $lastname; + } + + public function getLastname(): ?string + { + return $this->lastname; + } + + public function addArticle(Article $article): void + { + $this->articles[] = $article; + } + + /** + * @return Collection + */ + public function getArticles(): Collection + { + return $this->articles; + } +} diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Child.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Child.php index 108e90d8c9..683ebaec00 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Child.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Child.php @@ -1,25 +1,37 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Child extends MappedSuperclass { /** * @ORM\Column(name="title", type="string") */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING)] + private ?string $title = null; - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Comment.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Comment.php index e7e24a065d..e58dcda565 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Comment.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Comment.php @@ -1,64 +1,87 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class Comment { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="comment", type="string") */ - private $comment; + #[ORM\Column(name: 'comment', type: Types::STRING)] + private ?string $comment = null; /** * @ORM\Column(name="deletedAt", type="datetime", nullable=true) */ - private $deletedAt; + #[ORM\Column(name: 'deletedAt', type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; /** + * @var Article|null + * * @ORM\ManyToOne(targetEntity="Article", inversedBy="comments") */ + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'comments')] private $article; - public function getId() + public function getId(): ?int { return $this->id; } - public function setComment($comment) + public function setComment(?string $comment): void { $this->comment = $comment; } - public function getComment() + public function getComment(): ?string { return $this->comment; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(?\DateTime $deletedAt): void { $this->deletedAt = $deletedAt; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } - public function getArticle() + public function getArticle(): ?Article { return $this->article; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/MappedSuperclass.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/MappedSuperclass.php index a5725181c0..93cb455692 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/MappedSuperclass.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/MappedSuperclass.php @@ -1,39 +1,58 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\MappedSuperclass + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ORM\MappedSuperclass] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class MappedSuperclass { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="deletedAt", type="datetime", nullable=true) */ - private $deletedAt; + #[ORM\Column(name: 'deletedAt', type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(?\DateTime $deletedAt): void { $this->deletedAt = $deletedAt; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/MegaPage.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/MegaPage.php index baa6970d7e..2d52e30020 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/MegaPage.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/MegaPage.php @@ -1,12 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class MegaPage extends Page { } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Module.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Module.php index 08ca9b84bc..7ced4c2876 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Module.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Module.php @@ -1,69 +1,90 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class Module { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string") */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING)] + private ?string $title = null; /** * @ORM\Column(name="deletedAt", type="datetime", nullable=true) */ - private $deletedAt; + #[ORM\Column(name: 'deletedAt', type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; /** * @ORM\ManyToOne(targetEntity="Page", inversedBy="modules") */ - private $page; + #[ORM\ManyToOne(targetEntity: Page::class, inversedBy: 'modules')] + private ?Page $page = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setPage(Page $page) + public function setPage(Page $page): void { $this->page = $page; } - public function getPage() + public function getPage(): ?Page { return $this->page; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(?\DateTime $deletedAt): void { $this->deletedAt = $deletedAt; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/OtherArticle.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/OtherArticle.php index c7b55ef29a..f3692c3c7e 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/OtherArticle.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/OtherArticle.php @@ -1,37 +1,61 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; -use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class OtherArticle { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string") */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING)] + private ?string $title = null; /** * @ORM\Column(name="deletedAt", type="datetime", nullable=true) */ - private $deletedAt; + #[ORM\Column(name: 'deletedAt', type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="OtherComment", mappedBy="article") */ + #[ORM\OneToMany(targetEntity: OtherComment::class, mappedBy: 'article')] private $comments; public function __construct() @@ -39,32 +63,32 @@ public function __construct() $this->comments = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(\DateTime $deletedAt): void { $this->deletedAt = $deletedAt; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } - public function addComment(OtherComment $comment) + public function addComment(OtherComment $comment): void { $this->comments[] = $comment; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/OtherComment.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/OtherComment.php index d6c2c99f86..d76529ce47 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/OtherComment.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/OtherComment.php @@ -1,62 +1,82 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class OtherComment { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="comment", type="string") */ - private $comment; + #[ORM\Column(name: 'comment', type: Types::STRING)] + private ?string $comment = null; /** * @ORM\ManyToOne(targetEntity="OtherArticle", inversedBy="comments") */ - private $article; + #[ORM\ManyToOne(targetEntity: OtherArticle::class, inversedBy: 'comments')] + private ?OtherArticle $article = null; + + private ?\DateTimeInterface $deletedAt = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setComment($comment) + public function setComment(?string $comment): void { $this->comment = $comment; } - public function getComment() + public function getComment(): ?string { return $this->comment; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(?\DateTimeInterface $deletedAt): void { $this->deletedAt = $deletedAt; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTimeInterface { return $this->deletedAt; } - public function setArticle(OtherArticle $article) + public function setArticle(OtherArticle $article): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?OtherArticle { return $this->article; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Page.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Page.php index 080fdf02b0..8cf55a05a7 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Page.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Page.php @@ -1,40 +1,67 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; -use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="discr", type="string") - * @ORM\DiscriminatorMap({"page" = "Page", "mega_page" = "MegaPage"}) + * @ORM\DiscriminatorMap({"page": "Page", "mega_page": "MegaPage"}) + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ORM\Entity] +#[ORM\InheritanceType('JOINED')] +#[ORM\DiscriminatorColumn(name: 'discr', type: Types::STRING)] +#[ORM\DiscriminatorMap(['page' => Page::class, 'mega_page' => MegaPage::class])] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class Page { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string") */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING)] + private ?string $title = null; /** * @ORM\Column(name="deletedAt", type="datetime", nullable=true) */ - private $deletedAt; + #[ORM\Column(name: 'deletedAt', type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Module", mappedBy="page", cascade={"persist", "remove"}) */ + #[ORM\OneToMany(targetEntity: Module::class, mappedBy: 'page', cascade: ['persist', 'remove'])] private $modules; public function __construct() @@ -42,33 +69,33 @@ public function __construct() $this->modules = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(?\DateTime $deletedAt): void { $this->deletedAt = $deletedAt; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } - public function addModule(Module $module) + public function addModule(Module $module): void { - $this->module[] = $module; + $this->modules[] = $module; } } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Person.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Person.php index 4d65b831b6..cbec15111f 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Person.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Person.php @@ -1,56 +1,77 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=true) */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', timeAware: true)] class Person { /** - * @ORM\Column(type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=32) */ - private $name; + #[ORM\Column(length: 32)] + private ?string $name = null; /** - * @ORM\Column(type="datetime", nullable=true) + * @ORM\Column(name="deletedAt", type="datetime", nullable=true) */ - private $deletedAt; + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; /** * @ORM\OneToOne(targetEntity="Address", inversedBy="owner", cascade={"remove"}) */ - private $address; + #[ORM\OneToOne(targetEntity: Address::class, inversedBy: 'owner', cascade: ['remove'])] + private ?Address $address = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setName($name) + public function setName(?string $name): self { $this->name = $name; return $this; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setAddress(Address $address) + public function setAddress(Address $address): self { $this->address = $address; $address->setOwner($this); @@ -58,19 +79,19 @@ public function setAddress(Address $address) return $this; } - public function getAddress() + public function getAddress(): ?Address { return $this->address; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(?\DateTime $deletedAt): self { $this->deletedAt = $deletedAt; return $this; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/User.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/User.php index ed79983280..d835d9b711 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/User.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/User.php @@ -1,53 +1,74 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\SoftDeleteable(fieldName="deletedAt") */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] class User { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string") */ - private $username; + #[ORM\Column(name: 'title', type: Types::STRING)] + private ?string $username = null; /** * @ORM\Column(name="deleted_time", type="datetime", nullable=true) */ - private $deletedAt; + #[ORM\Column(name: 'deleted_time', type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setUsername($username) + public function setUsername(?string $username): void { $this->username = $username; } - public function getUsername() + public function getUsername(): ?string { return $this->username; } - public function setDeletedAt($deletedAt) + public function setDeletedAt(?\DateTime $deletedAt): void { $this->deletedAt = $deletedAt; } - public function getDeletedAt() + public function getDeletedAt(): ?\DateTime { return $this->deletedAt; } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/UserNoHardDelete.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/UserNoHardDelete.php new file mode 100644 index 0000000000..d2e7409063 --- /dev/null +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/UserNoHardDelete.php @@ -0,0 +1,75 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * + * @Gedmo\SoftDeleteable(fieldName="deletedAt", hardDelete=false) + */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', hardDelete: false)] +class UserNoHardDelete +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string") + */ + #[ORM\Column(name: 'title', type: Types::STRING)] + private ?string $username = null; + + /** + * @ORM\Column(name="deleted_time", type="datetime", nullable=true) + */ + #[ORM\Column(name: 'deleted_time', type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTime $deletedAt = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setUsername(?string $username): void + { + $this->username = $username; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setDeletedAt(?\DateTime $deletedAt): void + { + $this->deletedAt = $deletedAt; + } + + public function getDeletedAt(): ?\DateTime + { + return $this->deletedAt; + } +} diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/UsingTrait.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/UsingTrait.php index 6dfac98f69..e07db71182 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/UsingTrait.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/UsingTrait.php @@ -1,13 +1,20 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity; /** * Class UsingTrait - * - * @package SoftDeleteable\Fixture\Entity */ class UsingTrait { diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithLifecycleEventArgsFromODMTypeListener.php b/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithLifecycleEventArgsFromODMTypeListener.php new file mode 100644 index 0000000000..fdc592d810 --- /dev/null +++ b/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithLifecycleEventArgsFromODMTypeListener.php @@ -0,0 +1,35 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Listener; + +use Doctrine\Common\EventSubscriber; +use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs; +use Gedmo\SoftDeleteable\SoftDeleteableListener; + +final class WithLifecycleEventArgsFromODMTypeListener implements EventSubscriber +{ + public function preSoftDelete(LifecycleEventArgs $args): void + { + } + + public function postSoftDelete(LifecycleEventArgs $args): void + { + } + + public function getSubscribedEvents(): array + { + return [ + SoftDeleteableListener::PRE_SOFT_DELETE, + SoftDeleteableListener::POST_SOFT_DELETE, + ]; + } +} diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithLifecycleEventArgsFromORMTypeListener.php b/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithLifecycleEventArgsFromORMTypeListener.php new file mode 100644 index 0000000000..5d936b7d5f --- /dev/null +++ b/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithLifecycleEventArgsFromORMTypeListener.php @@ -0,0 +1,35 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Listener; + +use Doctrine\Common\EventSubscriber; +use Doctrine\ORM\Event\LifecycleEventArgs; +use Gedmo\SoftDeleteable\SoftDeleteableListener; + +final class WithLifecycleEventArgsFromORMTypeListener implements EventSubscriber +{ + public function preSoftDelete(LifecycleEventArgs $args): void + { + } + + public function postSoftDelete(LifecycleEventArgs $args): void + { + } + + public function getSubscribedEvents(): array + { + return [ + SoftDeleteableListener::PRE_SOFT_DELETE, + SoftDeleteableListener::POST_SOFT_DELETE, + ]; + } +} diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithPreAndPostSoftDeleteEventArgsTypeListener.php b/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithPreAndPostSoftDeleteEventArgsTypeListener.php new file mode 100644 index 0000000000..b24c51f6d7 --- /dev/null +++ b/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithPreAndPostSoftDeleteEventArgsTypeListener.php @@ -0,0 +1,39 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Listener; + +use Doctrine\Common\EventSubscriber; +use Doctrine\Persistence\ObjectManager; +use Gedmo\SoftDeleteable\Event\PostSoftDeleteEventArgs; +use Gedmo\SoftDeleteable\Event\PreSoftDeleteEventArgs; +use Gedmo\SoftDeleteable\SoftDeleteableListener; + +final class WithPreAndPostSoftDeleteEventArgsTypeListener implements EventSubscriber +{ + /** @param PreSoftDeleteEventArgs $args */ + public function preSoftDelete(PreSoftDeleteEventArgs $args): void + { + } + + /** @param PostSoftDeleteEventArgs $args */ + public function postSoftDelete(PostSoftDeleteEventArgs $args): void + { + } + + public function getSubscribedEvents(): array + { + return [ + SoftDeleteableListener::PRE_SOFT_DELETE, + SoftDeleteableListener::POST_SOFT_DELETE, + ]; + } +} diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithoutTypeListener.php b/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithoutTypeListener.php new file mode 100644 index 0000000000..9ab5cf3a8e --- /dev/null +++ b/tests/Gedmo/SoftDeleteable/Fixture/Listener/WithoutTypeListener.php @@ -0,0 +1,39 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Listener; + +use Doctrine\Common\EventSubscriber; +use Doctrine\Persistence\ObjectManager; +use Gedmo\SoftDeleteable\Event\PostSoftDeleteEventArgs; +use Gedmo\SoftDeleteable\Event\PreSoftDeleteEventArgs; +use Gedmo\SoftDeleteable\SoftDeleteableListener; + +final class WithoutTypeListener implements EventSubscriber +{ + /** @param PreSoftDeleteEventArgs $args */ + public function preSoftDelete($args): void + { + } + + /** @param PostSoftDeleteEventArgs $args */ + public function postSoftDelete($args): void + { + } + + public function getSubscribedEvents(): array + { + return [ + SoftDeleteableListener::PRE_SOFT_DELETE, + SoftDeleteableListener::POST_SOFT_DELETE, + ]; + } +} diff --git a/tests/Gedmo/SoftDeleteable/HardRelationTest.php b/tests/Gedmo/SoftDeleteable/HardRelationTest.php index 095d02fc1a..09bbabbb78 100644 --- a/tests/Gedmo/SoftDeleteable/HardRelationTest.php +++ b/tests/Gedmo/SoftDeleteable/HardRelationTest.php @@ -1,31 +1,37 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable; -use Tool\BaseTestCaseORM; use Doctrine\Common\EventManager; -use SoftDeleteable\Fixture\Entity\Person; -use SoftDeleteable\Fixture\Entity\Address; +use Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter; +use Gedmo\SoftDeleteable\SoftDeleteableListener; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Address; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Person; +use Gedmo\Tests\Tool\BaseTestCaseORM; -class HardRelationTest extends BaseTestCaseORM +final class HardRelationTest extends BaseTestCaseORM { - private $softDeleteableListener; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); - $evm->addEventSubscriber($this->softDeleteableListener = new SoftDeleteableListener()); - $this->getMockSqliteEntityManager($evm); - $this->em->getConfiguration()->addFilter('softdelete', 'Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter'); + $evm->addEventSubscriber(new SoftDeleteableListener()); + $this->getDefaultMockSqliteEntityManager($evm); + $this->em->getConfiguration()->addFilter('softdelete', SoftDeleteableFilter::class); $this->em->getFilters()->enable('softdelete'); } - /** - * @test - */ - public function shouldCascadeSoftdeleteForHardRelations() + public function testShouldCascadeSoftdeleteForHardRelations(): void { $address = new Address(); $address->setStreet('13 Boulangerie, 404'); @@ -43,14 +49,11 @@ public function shouldCascadeSoftdeleteForHardRelations() $this->em->flush(); $this->em->clear(); - $person = $this->em->getRepository('SoftDeleteable\Fixture\Entity\Person')->findOneById($person->getId()); - $this->assertNull($person, "Softdelete should cascade to hard relation entity"); + $person = $this->em->getRepository(Person::class)->findOneBy(['id' => $person->getId()]); + static::assertNull($person, 'Softdelete should cascade to hard relation entity'); } - /** - * @test - */ - public function shouldCascadeToInversedRelationAsWell() + public function testShouldCascadeToInversedRelationAsWell(): void { $address = new Address(); $address->setStreet('13 Boulangerie, 404'); @@ -68,14 +71,11 @@ public function shouldCascadeToInversedRelationAsWell() $this->em->flush(); $this->em->clear(); - $address = $this->em->getRepository('SoftDeleteable\Fixture\Entity\Address')->findOneById($address->getId()); - $this->assertNull($address, "Softdelete should cascade to hard relation entity"); + $address = $this->em->getRepository(Address::class)->findOneBy(['id' => $address->getId()]); + static::assertNull($address, 'Softdelete should cascade to hard relation entity'); } - /** - * @test - */ - public function shouldHandleTimeAwareSoftDeleteable() + public function testShouldHandleTimeAwareSoftDeleteable(): void { $address = new Address(); $address->setStreet('13 Boulangerie, 404'); @@ -90,23 +90,23 @@ public function shouldHandleTimeAwareSoftDeleteable() $this->em->flush(); $this->em->clear(); - $person = $this->em->getRepository('SoftDeleteable\Fixture\Entity\Person')->findOneById($person->getId()); - $this->assertNotNull($person, "Should not be softdeleted"); + $person = $this->em->getRepository(Person::class)->findOneBy(['id' => $person->getId()]); + static::assertNotNull($person, 'Should not be softdeleted'); $person->setDeletedAt(new \DateTime(date('Y-m-d H:i:s', time() - 15 * 3600))); // in an hour $this->em->persist($person); $this->em->flush(); $this->em->clear(); - $person = $this->em->getRepository('SoftDeleteable\Fixture\Entity\Person')->findOneById($person->getId()); - $this->assertNull($person, "Should be softdeleted"); + $person = $this->em->getRepository(Person::class)->findOneBy(['id' => $person->getId()]); + static::assertNull($person, 'Should be softdeleted'); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - 'SoftDeleteable\Fixture\Entity\Person', - 'SoftDeleteable\Fixture\Entity\Address', - ); + return [ + Person::class, + Address::class, + ]; } } diff --git a/tests/Gedmo/SoftDeleteable/SoftDeletableDocumentTraitTest.php b/tests/Gedmo/SoftDeleteable/SoftDeletableDocumentTraitTest.php index 2e2299e8e8..9ad3901397 100644 --- a/tests/Gedmo/SoftDeleteable/SoftDeletableDocumentTraitTest.php +++ b/tests/Gedmo/SoftDeleteable/SoftDeletableDocumentTraitTest.php @@ -1,41 +1,42 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable; + +use Gedmo\Tests\SoftDeleteable\Fixture\Document\UsingTrait; +use PHPUnit\Framework\TestCase; /** * Test for SoftDeletable Entity Trait * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SoftDeletableDocumentTraitTest extends \PHPUnit_Framework_TestCase +final class SoftDeletableDocumentTraitTest extends TestCase { /** * @var UsingTrait */ protected $entity; - public function setUp() - { - if (version_compare(PHP_VERSION, '5.4.0') < 0) { - $this->markTestSkipped('PHP >= 5.4 version required for this test.'); - } - } - - public function testGetSetDeletedAt() + public function testGetSetDeletedAt(): void { $time = new \DateTime(); $entity = new UsingTrait(); - $this->assertNull($entity->getDeletedAt(), 'deletedAt defaults to null'); - $this->assertFalse($entity->isDeleted(), 'isDeleted defaults to false'); - $this->assertSame($entity, $entity->setDeletedAt($time), 'Setter has a fluid interface'); - $this->assertSame($time, $entity->getDeletedAt(), 'Getter returns a DateTime Object'); - $this->assertTrue($entity->isDeleted(), 'Is deleted is true when deleteAt is not equal to null'); - $this->assertSame($entity, $entity->setDeletedAt(), 'Setting deletedAt to null undeletes object'); - $this->assertFalse($entity->isDeleted(), 'isDeleted should now return false'); + static::assertNull($entity->getDeletedAt(), 'deletedAt defaults to null'); + static::assertFalse($entity->isDeleted(), 'isDeleted defaults to false'); + static::assertSame($entity, $entity->setDeletedAt($time), 'Setter has a fluid interface'); + static::assertSame($time, $entity->getDeletedAt(), 'Getter returns a DateTime Object'); + static::assertTrue($entity->isDeleted(), 'Is deleted is true when deleteAt is not equal to null'); + static::assertSame($entity, $entity->setDeletedAt(), 'Setting deletedAt to null undeletes object'); + static::assertFalse($entity->isDeleted(), 'isDeleted should now return false'); } } diff --git a/tests/Gedmo/SoftDeleteable/SoftDeletableEntityTraitTest.php b/tests/Gedmo/SoftDeleteable/SoftDeletableEntityTraitTest.php index c729745702..aef5cdd6db 100644 --- a/tests/Gedmo/SoftDeleteable/SoftDeletableEntityTraitTest.php +++ b/tests/Gedmo/SoftDeleteable/SoftDeletableEntityTraitTest.php @@ -1,41 +1,42 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable; + +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\UsingTrait; +use PHPUnit\Framework\TestCase; /** * Test for SoftDeletable Entity Trait * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SoftDeletableEntityTraitTest extends \PHPUnit_Framework_TestCase +final class SoftDeletableEntityTraitTest extends TestCase { /** * @var UsingTrait */ protected $entity; - public function setUp() - { - if (version_compare(PHP_VERSION, '5.4.0') < 0) { - $this->markTestSkipped('PHP >= 5.4 version required for this test.'); - } - } - - public function testGetSetDeletedAt() + public function testGetSetDeletedAt(): void { $time = new \DateTime(); $entity = new UsingTrait(); - $this->assertNull($entity->getDeletedAt(), 'deletedAt defaults to null'); - $this->assertFalse($entity->isDeleted(), 'isDeleted defaults to false'); - $this->assertSame($entity, $entity->setDeletedAt($time), 'Setter has a fluid interface'); - $this->assertSame($time, $entity->getDeletedAt(), 'Getter returns a DateTime Object'); - $this->assertTrue($entity->isDeleted(), 'Is deleted is true when deleteAt is not equal to null'); - $this->assertSame($entity, $entity->setDeletedAt(), 'Setting deletedAt to null undeletes object'); - $this->assertFalse($entity->isDeleted(), 'isDeleted should now return false'); + static::assertNull($entity->getDeletedAt(), 'deletedAt defaults to null'); + static::assertFalse($entity->isDeleted(), 'isDeleted defaults to false'); + static::assertSame($entity, $entity->setDeletedAt($time), 'Setter has a fluid interface'); + static::assertSame($time, $entity->getDeletedAt(), 'Getter returns a DateTime Object'); + static::assertTrue($entity->isDeleted(), 'Is deleted is true when deleteAt is not equal to null'); + static::assertSame($entity, $entity->setDeletedAt(), 'Setting deletedAt to null undeletes object'); + static::assertFalse($entity->isDeleted(), 'isDeleted should now return false'); } } diff --git a/tests/Gedmo/SoftDeleteable/SoftDeleteableDocumentTest.php b/tests/Gedmo/SoftDeleteable/SoftDeleteableDocumentTest.php index 3053297094..b2c1fe4da6 100644 --- a/tests/Gedmo/SoftDeleteable/SoftDeleteableDocumentTest.php +++ b/tests/Gedmo/SoftDeleteable/SoftDeleteableDocumentTest.php @@ -1,18 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use SoftDeleteable\Fixture\Document\Article; -use SoftDeleteable\Fixture\Document\Comment; -use SoftDeleteable\Fixture\Document\User; -use SoftDeleteable\Fixture\Document\Page; -use SoftDeleteable\Fixture\Document\MegaPage; -use SoftDeleteable\Fixture\Document\Module; -use SoftDeleteable\Fixture\Document\OtherArticle; -use SoftDeleteable\Fixture\Document\OtherComment; -use SoftDeleteable\Fixture\Document\Child; +use Gedmo\SoftDeleteable\Filter\ODM\SoftDeleteableFilter; +use Gedmo\SoftDeleteable\SoftDeleteableListener; +use Gedmo\Tests\SoftDeleteable\Fixture\Document\User; +use Gedmo\Tests\SoftDeleteable\Fixture\Document\UserTimeAware; +use Gedmo\Tests\SoftDeleteable\Fixture\Listener\WithLifecycleEventArgsFromODMTypeListener; +use Gedmo\Tests\SoftDeleteable\Fixture\Listener\WithoutTypeListener; +use Gedmo\Tests\SoftDeleteable\Fixture\Listener\WithPreAndPostSoftDeleteEventArgsTypeListener; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for SoftDeleteable behavior @@ -20,45 +27,30 @@ * @author Gustavo Falco * @author Gediminas Morkevicius * @author Patrik Votoฤek - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SoftDeleteableDocumentTest extends BaseTestCaseMongoODM +final class SoftDeleteableDocumentTest extends BaseTestCaseMongoODM { - const ARTICLE_CLASS = 'SoftDeleteable\Fixture\Document\Article'; - const COMMENT_CLASS = 'SoftDeleteable\Fixture\Document\Comment'; - const PAGE_CLASS = 'SoftDeleteable\Fixture\Document\Page'; - const MEGA_PAGE_CLASS = 'SoftDeleteable\Fixture\Document\MegaPage'; - const MODULE_CLASS = 'SoftDeleteable\Fixture\Document\Module'; - const OTHER_ARTICLE_CLASS = 'SoftDeleteable\Fixture\Document\OtherArticle'; - const OTHER_COMMENT_CLASS = 'SoftDeleteable\Fixture\Document\OtherComment'; - const USER_CLASS = 'SoftDeleteable\Fixture\Document\User'; - const USER__TIME_AWARE_CLASS = 'SoftDeleteable\Fixture\Document\UserTimeAware'; - const MAPPED_SUPERCLASS_CHILD_CLASS = 'SoftDeleteable\Fixture\Document\Child'; - const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable'; - - private $softDeleteableListener; - - protected function setUp() + private const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable'; + + private SoftDeleteableListener $softDeleteableListener; + + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $this->softDeleteableListener = new SoftDeleteableListener(); $evm->addEventSubscriber($this->softDeleteableListener); - $config = $this->getMockAnnotatedConfig(); - $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, 'Gedmo\SoftDeleteable\Filter\ODM\SoftDeleteableFilter'); + $config = $this->getDefaultConfiguration(); + $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, SoftDeleteableFilter::class); $this->dm = $this->getMockDocumentManager($evm, $config); $this->dm->getFilterCollection()->enable(self::SOFT_DELETEABLE_FILTER_NAME); } - /** - * @test - */ - public function shouldSoftlyDeleteIfColumnNameDifferFromPropertyName() + public function testShouldSoftlyDeleteIfColumnNameDifferFromPropertyName(): void { - $repo = $this->dm->getRepository(self::USER_CLASS); + $repo = $this->dm->getRepository(User::class); $newUser = new User(); @@ -68,30 +60,29 @@ public function shouldSoftlyDeleteIfColumnNameDifferFromPropertyName() $this->dm->persist($newUser); $this->dm->flush(); - $user = $repo->findOneBy(array('username' => $username)); + $user = $repo->findOneBy(['username' => $username]); - $this->assertNull($user->getDeletedAt()); + static::assertNull($user->getDeletedAt()); $this->dm->remove($user); $this->dm->flush(); - $user = $repo->findOneBy(array('username' => $username)); + $user = $repo->findOneBy(['username' => $username]); - $this->assertNull($user); + static::assertNull($user); } /** * Tests the filter by enabling and disabling it between * some user persists actions. - * - * @test */ - public function testSoftDeleteableFilter() + public function testSoftDeleteableFilter(): void { $filter = $this->dm->getFilterCollection()->getFilter(self::SOFT_DELETEABLE_FILTER_NAME); - $filter->disableForDocument(self::USER_CLASS); + static::assertInstanceOf(SoftDeleteableFilter::class, $filter); + $filter->disableForDocument(User::class); - $repo = $this->dm->getRepository(self::USER_CLASS); + $repo = $this->dm->getRepository(User::class); $newUser = new User(); $username = 'test_user'; @@ -99,20 +90,20 @@ public function testSoftDeleteableFilter() $this->dm->persist($newUser); $this->dm->flush(); - $user = $repo->findOneBy(array('username' => $username)); + $user = $repo->findOneBy(['username' => $username]); - $this->assertNull($user->getDeletedAt()); + static::assertNull($user->getDeletedAt()); $this->dm->remove($user); $this->dm->flush(); - $user = $repo->findOneBy(array('username' => $username)); + $user = $repo->findOneBy(['username' => $username]); - $this->assertNotNull($user->getDeletedAt()); + static::assertNotNull($user->getDeletedAt()); - $filter->enableForDocument(self::USER_CLASS); + $filter->enableForDocument(User::class); - $user = $repo->findOneBy(array('username' => $username)); - $this->assertNull($user); + $user = $repo->findOneBy(['username' => $username]); + static::assertNull($user); } /** @@ -122,14 +113,15 @@ public function testSoftDeleteableFilter() * @TODO: not supported in ODM yet * test */ - public function shouldSupportSoftDeleteableFilterTimeAware() + public function shouldSupportSoftDeleteableFilterTimeAware(): void { $filter = $this->dm->getFilterCollection()->getFilter(self::SOFT_DELETEABLE_FILTER_NAME); - $filter->disableForDocument(self::USER__TIME_AWARE_CLASS); + static::assertInstanceOf(SoftDeleteableFilter::class, $filter); + $filter->disableForDocument(UserTimeAware::class); - $repo = $this->dm->getRepository(self::USER__TIME_AWARE_CLASS); + $repo = $this->dm->getRepository(UserTimeAware::class); - //Find entity with deletedAt date in future + // Find entity with deletedAt date in future $newUser = new User(); $username = 'test_user'; $newUser->setUsername($username); @@ -137,12 +129,12 @@ public function shouldSupportSoftDeleteableFilterTimeAware() $this->dm->persist($newUser); $this->dm->flush(); - $user = $repo->findOneBy(array('username' => $username)); + $user = $repo->findOneBy(['username' => $username]); $this->dm->remove($user); $this->dm->flush(); - //Don't find entity with deletedAt date in past + // Don't find entity with deletedAt date in past $newUser = new User(); $username = 'test_user'; $newUser->setUsername($username); @@ -150,38 +142,31 @@ public function shouldSupportSoftDeleteableFilterTimeAware() $this->dm->persist($newUser); $this->dm->flush(); - $user = $repo->findOneBy(array('username' => $username)); + $user = $repo->findOneBy(['username' => $username]); - $this->assertNull($user); - $this->dm->remove($user); + static::assertNull($user); $this->dm->flush(); } - public function testPostSoftDeleteEventIsDispatched() - { - $subscriber = $this->getMock( - "Doctrine\Common\EventSubscriber", - array( - "getSubscribedEvents", - "preSoftDelete", - "postSoftDelete", - ) - ); - $subscriber->expects($this->once()) - ->method("getSubscribedEvents") - ->will($this->returnValue(array(SoftDeleteableListener::PRE_SOFT_DELETE, SoftDeleteableListener::POST_SOFT_DELETE))); + public function testPostSoftDeleteEventIsDispatched(): void + { + $this->dm->getEventManager()->addEventSubscriber(new WithPreAndPostSoftDeleteEventArgsTypeListener()); - $subscriber->expects($this->once()) - ->method("preSoftDelete") - ->with($this->anything()); + $this->doTestPostSoftDeleteEventIsDispatched(); + } - $subscriber->expects($this->once()) - ->method("postSoftDelete") - ->with($this->anything()); + /** @group legacy */ + public function testPostSoftDeleteEventIsDispatchedWithDeprecatedListeners(): void + { + $this->dm->getEventManager()->addEventSubscriber(new WithoutTypeListener()); + $this->dm->getEventManager()->addEventSubscriber(new WithLifecycleEventArgsFromODMTypeListener()); - $this->dm->getEventManager()->addEventSubscriber($subscriber); + $this->doTestPostSoftDeleteEventIsDispatched(); + } - $repo = $this->dm->getRepository(self::USER_CLASS); + private function doTestPostSoftDeleteEventIsDispatched(): void + { + $repo = $this->dm->getRepository(User::class); $newUser = new User(); $username = 'test_user'; @@ -190,9 +175,9 @@ public function testPostSoftDeleteEventIsDispatched() $this->dm->persist($newUser); $this->dm->flush(); - $user = $repo->findOneBy(array('username' => 'test_user')); + $user = $repo->findOneBy(['username' => 'test_user']); - $this->assertNull($user->getDeletedAt()); + static::assertNull($user->getDeletedAt()); $this->dm->remove($user); $this->dm->flush(); diff --git a/tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php b/tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php index 8157f69f22..129351b3b9 100644 --- a/tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php +++ b/tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php @@ -1,18 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable; -use Tool\BaseTestCaseORM; use Doctrine\Common\EventManager; -use SoftDeleteable\Fixture\Entity\Article; -use SoftDeleteable\Fixture\Entity\Comment; -use SoftDeleteable\Fixture\Entity\User; -use SoftDeleteable\Fixture\Entity\Page; -use SoftDeleteable\Fixture\Entity\MegaPage; -use SoftDeleteable\Fixture\Entity\Module; -use SoftDeleteable\Fixture\Entity\OtherArticle; -use SoftDeleteable\Fixture\Entity\OtherComment; -use SoftDeleteable\Fixture\Entity\Child; +use Doctrine\ORM\Event\LifecycleEventArgs; +use Doctrine\ORM\Query; +use Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter; +use Gedmo\SoftDeleteable\Query\TreeWalker\SoftDeleteableWalker; +use Gedmo\SoftDeleteable\SoftDeleteableListener; +use Gedmo\Tests\Clock; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Article; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Author; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Child; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Comment; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\MegaPage; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Module; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\OtherArticle; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\OtherComment; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Page; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\User; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\UserNoHardDelete; +use Gedmo\Tests\SoftDeleteable\Fixture\Listener\WithLifecycleEventArgsFromORMTypeListener; +use Gedmo\Tests\SoftDeleteable\Fixture\Listener\WithoutTypeListener; +use Gedmo\Tests\SoftDeleteable\Fixture\Listener\WithPreAndPostSoftDeleteEventArgsTypeListener; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Symfony\Component\Cache\Adapter\ArrayAdapter; /** * These are tests for SoftDeleteable behavior @@ -20,43 +41,30 @@ * @author Gustavo Falco * @author Gediminas Morkevicius * @author Patrik Votoฤek - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SoftDeleteableEntityTest extends BaseTestCaseORM +final class SoftDeleteableEntityTest extends BaseTestCaseORM { - const ARTICLE_CLASS = 'SoftDeleteable\Fixture\Entity\Article'; - const COMMENT_CLASS = 'SoftDeleteable\Fixture\Entity\Comment'; - const PAGE_CLASS = 'SoftDeleteable\Fixture\Entity\Page'; - const MEGA_PAGE_CLASS = 'SoftDeleteable\Fixture\Entity\MegaPage'; - const MODULE_CLASS = 'SoftDeleteable\Fixture\Entity\Module'; - const OTHER_ARTICLE_CLASS = 'SoftDeleteable\Fixture\Entity\OtherArticle'; - const OTHER_COMMENT_CLASS = 'SoftDeleteable\Fixture\Entity\OtherComment'; - const USER_CLASS = 'SoftDeleteable\Fixture\Entity\User'; - const MAPPED_SUPERCLASS_CHILD_CLASS = 'SoftDeleteable\Fixture\Entity\Child'; - const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable'; - - private $softDeleteableListener; - - protected function setUp() + private const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable'; + + private SoftDeleteableListener $softDeleteableListener; + + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $this->softDeleteableListener = new SoftDeleteableListener(); + $this->softDeleteableListener->setClock(new Clock()); $evm->addEventSubscriber($this->softDeleteableListener); - $config = $this->getMockAnnotatedConfig(); - $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, 'Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter'); - $this->em = $this->getMockSqliteEntityManager($evm, $config); + $config = $this->getDefaultConfiguration(); + $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, SoftDeleteableFilter::class); + $this->em = $this->getDefaultMockSqliteEntityManager($evm, $config); $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); } - /** - * @test - */ - public function shouldBeAbleToHardDeleteSoftdeletedItems() + public function testShouldBeAbleToHardDeleteSoftdeletedItems(): void { - $repo = $this->em->getRepository(self::USER_CLASS); + $repo = $this->em->getRepository(User::class); $newUser = new User(); $newUser->setUsername($username = 'test_user'); @@ -64,22 +72,19 @@ public function shouldBeAbleToHardDeleteSoftdeletedItems() $this->em->persist($newUser); $this->em->flush(); - $user = $repo->findOneBy(array('username' => $username)); - $this->assertNull($user->getDeletedAt()); + $user = $repo->findOneBy(['username' => $username]); + static::assertNull($user->getDeletedAt()); $this->em->remove($user); $this->em->flush(); - $user = $repo->findOneBy(array('username' => $username)); - $this->assertNull($user); + $user = $repo->findOneBy(['username' => $username]); + static::assertNull($user); } - /** - * @test - */ - public function shouldSoftlyDeleteIfColumnNameDifferFromPropertyName() + public function testShouldSoftlyDeleteIfColumnNameDifferFromPropertyName(): void { - $repo = $this->em->getRepository(self::USER_CLASS); + $repo = $this->em->getRepository(User::class); $newUser = new User(); $username = 'test_user'; @@ -88,32 +93,32 @@ public function shouldSoftlyDeleteIfColumnNameDifferFromPropertyName() $this->em->persist($newUser); $this->em->flush(); - $user = $repo->findOneBy(array('username' => $username)); + $user = $repo->findOneBy(['username' => $username]); - $this->assertNull($user->getDeletedAt()); + static::assertNull($user->getDeletedAt()); $this->em->remove($user); $this->em->flush(); - $user = $repo->findOneBy(array('username' => $username)); - $this->assertNull($user, "User should be filtered out"); + $user = $repo->findOneBy(['username' => $username]); + static::assertNull($user, 'User should be filtered out'); // now deactivate filter and attempt to hard delete $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); - $user = $repo->findOneBy(array('username' => $username)); - $this->assertNotNull($user, "User should be fetched when filter is disabled"); + $user = $repo->findOneBy(['username' => $username]); + static::assertNotNull($user, 'User should be fetched when filter is disabled'); $this->em->remove($user); $this->em->flush(); - $user = $repo->findOneBy(array('username' => $username)); - $this->assertNull($user, "User is still available after hard delete"); + $user = $repo->findOneBy(['username' => $username]); + static::assertNull($user, 'User is still available after hard delete'); } - public function testSoftDeleteable() + public function testSoftDeleteable(): void { - $repo = $this->em->getRepository(self::ARTICLE_CLASS); - $commentRepo = $this->em->getRepository(self::COMMENT_CLASS); + $repo = $this->em->getRepository(Article::class); + $commentRepo = $this->em->getRepository(Comment::class); $comment = new Comment(); $commentField = 'comment'; @@ -128,32 +133,32 @@ public function testSoftDeleteable() $this->em->persist($art0); $this->em->flush(); - $art = $repo->findOneBy(array($field => $value)); + $art = $repo->findOneBy([$field => $value]); - $this->assertNull($art->getDeletedAt()); - $this->assertNull($comment->getDeletedAt()); + static::assertNull($art->getDeletedAt()); + static::assertNull($comment->getDeletedAt()); $this->em->remove($art); $this->em->flush(); - $art = $repo->findOneBy(array($field => $value)); - $this->assertNull($art); - $comment = $commentRepo->findOneBy(array($commentField => $commentValue)); - $this->assertNull($comment); + $art = $repo->findOneBy([$field => $value]); + static::assertNull($art); + $comment = $commentRepo->findOneBy([$commentField => $commentValue]); + static::assertNull($comment); // Now we deactivate the filter so we test if the entity appears in the result $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); - $art = $repo->findOneBy(array($field => $value)); - $this->assertTrue(is_object($art)); - $this->assertTrue(is_object($art->getDeletedAt())); - $this->assertTrue($art->getDeletedAt() instanceof \DateTime); - $comment = $commentRepo->findOneBy(array($commentField => $commentValue)); - $this->assertTrue(is_object($comment)); - $this->assertTrue(is_object($comment->getDeletedAt())); - $this->assertTrue($comment->getDeletedAt() instanceof \DateTime); + $art = $repo->findOneBy([$field => $value]); + static::assertIsObject($art); + static::assertIsObject($art->getDeletedAt()); + static::assertInstanceOf(\DateTime::class, $art->getDeletedAt()); + $comment = $commentRepo->findOneBy([$commentField => $commentValue]); + static::assertIsObject($comment); + static::assertIsObject($comment->getDeletedAt()); + static::assertInstanceOf(\DateTime::class, $comment->getDeletedAt()); - $this->em->createQuery('UPDATE '.self::ARTICLE_CLASS.' a SET a.deletedAt = NULL')->execute(); + $this->em->createQuery('UPDATE '.Article::class.' a SET a.deletedAt = NULL')->execute(); $this->em->refresh($art); $this->em->refresh($comment); @@ -161,33 +166,33 @@ public function testSoftDeleteable() // Now we try with a DQL Delete query $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); $dql = sprintf('DELETE FROM %s a WHERE a.%s = :%s', - self::ARTICLE_CLASS, $field, $field); + Article::class, $field, $field); $query = $this->em->createQuery($dql); $query->setParameter($field, $value); $query->setHint( - \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, - 'Gedmo\SoftDeleteable\Query\TreeWalker\SoftDeleteableWalker' + Query::HINT_CUSTOM_OUTPUT_WALKER, + SoftDeleteableWalker::class ); $query->execute(); - $art = $repo->findOneBy(array($field => $value)); - $this->assertNull($art); + $art = $repo->findOneBy([$field => $value]); + static::assertNull($art); // Now we deactivate the filter so we test if the entity appears in the result $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); $this->em->clear(); - $art = $repo->findOneBy(array($field => $value)); + $art = $repo->findOneBy([$field => $value]); - $this->assertTrue(is_object($art)); - $this->assertTrue(is_object($art->getDeletedAt())); - $this->assertTrue($art->getDeletedAt() instanceof \DateTime); + static::assertIsObject($art); + static::assertIsObject($art->getDeletedAt()); + static::assertInstanceOf(\DateTime::class, $art->getDeletedAt()); // Inheritance tree DELETE DQL $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); - $megaPageRepo = $this->em->getRepository(self::MEGA_PAGE_CLASS); + $megaPageRepo = $this->em->getRepository(MegaPage::class); $module = new Module(); $module->setTitle('Module 1'); $page = new MegaPage(); @@ -200,33 +205,33 @@ public function testSoftDeleteable() $this->em->flush(); $dql = sprintf('DELETE FROM %s p', - self::PAGE_CLASS); + Page::class); $query = $this->em->createQuery($dql); $query->setHint( - \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, - 'Gedmo\SoftDeleteable\Query\TreeWalker\SoftDeleteableWalker' + Query::HINT_CUSTOM_OUTPUT_WALKER, + SoftDeleteableWalker::class ); $query->execute(); - $p = $megaPageRepo->findOneBy(array('title' => 'Page 1')); - $this->assertNull($p); + $p = $megaPageRepo->findOneBy(['title' => 'Page 1']); + static::assertNull($p); // Now we deactivate the filter so we test if the entity appears in the result $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); $this->em->clear(); - $p = $megaPageRepo->findOneBy(array('title' => 'Page 1')); + $p = $megaPageRepo->findOneBy(['title' => 'Page 1']); - $this->assertTrue(is_object($p)); - $this->assertTrue(is_object($p->getDeletedAt())); - $this->assertTrue($p->getDeletedAt() instanceof \DateTime); + static::assertIsObject($p); + static::assertIsObject($p->getDeletedAt()); + static::assertInstanceOf(\DateTime::class, $p->getDeletedAt()); // Test of #301 $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); - $otherArticleRepo = $this->em->getRepository(self::OTHER_ARTICLE_CLASS); - $otherCommentRepo = $this->em->getRepository(self::OTHER_COMMENT_CLASS); + $otherArticleRepo = $this->em->getRepository(OtherArticle::class); + $otherCommentRepo = $this->em->getRepository(OtherComment::class); $otherArt = new OtherArticle(); $otherComment = new OtherComment(); $otherArt->setTitle('Page 1'); @@ -247,29 +252,185 @@ public function testSoftDeleteable() $this->em->remove($otherArt); $this->em->flush(); - $foundArt = $otherArticleRepo->findOneBy(array('id' => $artId)); - $foundComment = $otherCommentRepo->findOneBy(array('id' => $commentId)); + $foundArt = $otherArticleRepo->findOneBy(['id' => $artId]); + $foundComment = $otherCommentRepo->findOneBy(['id' => $commentId]); + + static::assertNull($foundArt); + static::assertIsObject($foundComment); + static::assertInstanceOf(OtherComment::class, $foundComment); + + $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); + + $foundArt = $otherArticleRepo->findOneBy(['id' => $artId]); + $foundComment = $otherCommentRepo->findOneBy(['id' => $commentId]); + + static::assertIsObject($foundArt); + static::assertIsObject($foundArt->getDeletedAt()); + static::assertInstanceOf(\DateTime::class, $foundArt->getDeletedAt()); + static::assertIsObject($foundComment); + static::assertInstanceOf(OtherComment::class, $foundComment); + } + + /** + * @group datetimeinterface + */ + public function testSoftDeleteableWithDateTimeInterface(): void + { + $repo = $this->em->getRepository(Article::class); + $commentRepo = $this->em->getRepository(Comment::class); + + $comment = new Comment(); + $commentField = 'comment'; + $commentValue = 'Comment 1'; + $comment->setComment($commentValue); + $art0 = new Article(); + $field = 'title'; + $value = 'Title 1'; + $art0->setTitle($value); + $art0->addComment($comment); + + $this->em->persist($art0); + $this->em->flush(); + + $art = $repo->findOneBy([$field => $value]); + + static::assertNull($art->getDeletedAt()); + static::assertNull($comment->getDeletedAt()); + + $art->setDeletedAt(new \DateTime()); + $this->em->flush(); + + $art = $repo->findOneBy([$field => $value]); + static::assertNull($art); + + // Now we deactivate the filter so we test if the entity appears in the result + $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); + + $art = $repo->findOneBy([$field => $value]); + static::assertIsObject($art); + static::assertIsObject($art->getDeletedAt()); + static::assertInstanceOf('DateTimeInterface', $art->getDeletedAt()); + $comment = $commentRepo->findOneBy([$commentField => $commentValue]); + static::assertIsObject($comment); + static::assertNull($comment->getDeletedAt()); + + $this->em->createQuery('UPDATE '.Article::class.' a SET a.deletedAt = NULL')->execute(); + + $this->em->refresh($art); + $this->em->refresh($comment); + + // Now we try with a DQL Delete query + $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); + $dql = sprintf('DELETE FROM %s a WHERE a.%s = :%s', + Article::class, $field, $field); + $query = $this->em->createQuery($dql); + $query->setParameter($field, $value); + $query->setHint( + Query::HINT_CUSTOM_OUTPUT_WALKER, + SoftDeleteableWalker::class + ); + + $query->execute(); + + $art = $repo->findOneBy([$field => $value]); + static::assertNull($art); + + // Now we deactivate the filter so we test if the entity appears in the result + $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); + $this->em->clear(); + + $art = $repo->findOneBy([$field => $value]); + + static::assertIsObject($art); + static::assertIsObject($art->getDeletedAt()); + static::assertInstanceOf('DateTimeInterface', $art->getDeletedAt()); + + // Inheritance tree DELETE DQL + $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); + + $megaPageRepo = $this->em->getRepository(MegaPage::class); + $module = new Module(); + $module->setTitle('Module 1'); + $page = new MegaPage(); + $page->setTitle('Page 1'); + $page->addModule($module); + $module->setPage($page); + + $this->em->persist($page); + $this->em->persist($module); + $this->em->flush(); + + $dql = sprintf('DELETE FROM %s p', + Page::class); + $query = $this->em->createQuery($dql); + $query->setHint( + Query::HINT_CUSTOM_OUTPUT_WALKER, + SoftDeleteableWalker::class + ); + + $query->execute(); + + $p = $megaPageRepo->findOneBy(['title' => 'Page 1']); + static::assertNull($p); + + // Now we deactivate the filter so we test if the entity appears in the result + $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); + $this->em->clear(); + + $p = $megaPageRepo->findOneBy(['title' => 'Page 1']); + + static::assertIsObject($p); + static::assertIsObject($p->getDeletedAt()); + static::assertInstanceOf('DateTimeInterface', $p->getDeletedAt()); + + // Test of #301 + $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); + + $otherArticleRepo = $this->em->getRepository(OtherArticle::class); + $otherCommentRepo = $this->em->getRepository(OtherComment::class); + $otherArt = new OtherArticle(); + $otherComment = new OtherComment(); + $otherArt->setTitle('Page 1'); + $otherComment->setComment('Comment'); + $otherArt->addComment($otherComment); + $otherComment->setArticle($otherArt); + + $this->em->persist($otherArt); + $this->em->persist($otherComment); + $this->em->flush(); + + $this->em->refresh($otherArt); + $this->em->refresh($otherComment); - $this->assertNull($foundArt); - $this->assertTrue(is_object($foundComment)); - $this->assertInstanceOf(self::OTHER_COMMENT_CLASS, $foundComment); + $artId = $otherArt->getId(); + $commentId = $otherComment->getId(); + + $otherArt->setDeletedAt(new \DateTime()); + $this->em->flush(); + + $foundArt = $otherArticleRepo->findOneBy(['id' => $artId]); + $foundComment = $otherCommentRepo->findOneBy(['id' => $commentId]); + + static::assertNull($foundArt); + static::assertIsObject($foundComment); + static::assertInstanceOf(OtherComment::class, $foundComment); $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); - $foundArt = $otherArticleRepo->findOneById($artId); - $foundComment = $otherCommentRepo->findOneById($commentId); + $foundArt = $otherArticleRepo->findOneBy(['id' => $artId]); + $foundComment = $otherCommentRepo->findOneBy(['id' => $commentId]); - $this->assertTrue(is_object($foundArt)); - $this->assertTrue(is_object($foundArt->getDeletedAt())); - $this->assertTrue($foundArt->getDeletedAt() instanceof \DateTime); - $this->assertTrue(is_object($foundComment)); - $this->assertInstanceOf(self::OTHER_COMMENT_CLASS, $foundComment); + static::assertIsObject($foundArt); + static::assertIsObject($foundArt->getDeletedAt()); + static::assertInstanceOf('DateTimeInterface', $foundArt->getDeletedAt()); + static::assertIsObject($foundComment); + static::assertInstanceOf(OtherComment::class, $foundComment); } /** * Make sure that soft delete also works when configured on a mapped superclass */ - public function testMappedSuperclass() + public function testMappedSuperclass(): void { $child = new Child(); $child->setTitle('test title'); @@ -281,19 +442,20 @@ public function testMappedSuperclass() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::MAPPED_SUPERCLASS_CHILD_CLASS); - $this->assertNull($repo->findOneById($child->getId())); + $repo = $this->em->getRepository(Child::class); + static::assertNull($repo->findOneBy(['id' => $child->getId()])); $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); - $this->assertNotNull($repo->findById($child->getId())); + static::assertNotNull($repo->findById($child->getId())); } - public function testSoftDeleteableFilter() + public function testSoftDeleteableFilter(): void { $filter = $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); - $filter->disableForEntity(self::USER_CLASS); + static::assertInstanceOf(SoftDeleteableFilter::class, $filter); + $filter->disableForEntity(User::class); - $repo = $this->em->getRepository(self::USER_CLASS); + $repo = $this->em->getRepository(User::class); $newUser = new User(); $username = 'test_user'; @@ -302,54 +464,76 @@ public function testSoftDeleteableFilter() $this->em->persist($newUser); $this->em->flush(); - $user = $repo->findOneBy(array('username' => $username)); + $user = $repo->findOneBy(['username' => $username]); - $this->assertNull($user->getDeletedAt()); + static::assertNull($user->getDeletedAt()); $this->em->remove($user); $this->em->flush(); - $user = $repo->findOneBy(array('username' => $username)); - $this->assertNotNull($user->getDeletedAt()); + $user = $repo->findOneBy(['username' => $username]); + static::assertNotNull($user->getDeletedAt()); - $filter->enableForEntity(self::USER_CLASS); + $filter->enableForEntity(User::class); - $user = $repo->findOneBy(array('username' => $username)); - $this->assertNull($user); + $user = $repo->findOneBy(['username' => $username]); + static::assertNull($user); } - public function testPostSoftDeleteEventIsDispatched() + public function testShouldFilterBeQueryCachedCorrectlyWhenToggledForEntity(): void { - $subscriber = $this->getMock( - "Doctrine\Common\EventSubscriber", - array( - "getSubscribedEvents", - "preSoftDelete", - "postSoftDelete", - ) - ); + $this->em->getConfiguration()->setQueryCache(new ArrayAdapter()); + + $filter = $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); + static::assertInstanceOf(SoftDeleteableFilter::class, $filter); + $filter->disableForEntity(User::class); - $subscriber->expects($this->once()) - ->method("getSubscribedEvents") - ->will($this->returnValue(array(SoftDeleteableListener::PRE_SOFT_DELETE, SoftDeleteableListener::POST_SOFT_DELETE))); + $repo = $this->em->getRepository(User::class); - $subscriber->expects($this->exactly(2)) - ->method("preSoftDelete") - ->with($this->anything()); + $newUser = new User(); + $username = 'test_user'; + $newUser->setUsername($username); + + $this->em->persist($newUser); + $this->em->flush(); - $subscriber->expects($this->exactly(2)) - ->method("postSoftDelete") - ->with($this->anything()); + $user = $repo->findOneBy(['username' => $username]); - $this->em->getEventManager()->addEventSubscriber($subscriber); + static::assertNull($user->getDeletedAt()); - $repo = $this->em->getRepository(self::ARTICLE_CLASS); - $commentRepo = $this->em->getRepository(self::COMMENT_CLASS); + $this->em->remove($user); + $this->em->flush(); + + $dql = 'SELECT u FROM '.User::class.' u WHERE u.username = :username'; + $q = $this->em->createQuery($dql) + ->setParameter('username', $username) + ; + $data = $q->getResult(); + static::assertCount(1, $data); + $user = $data[0]; + static::assertNotNull($user->getDeletedAt()); + + $filter->enableForEntity(User::class); + + // The result should be different even with the query cache enabled. + $q = $this->em->createQuery($dql) + ->setParameter('username', $username) + ; + $data = $q->getResult(); + static::assertCount(0, $data); + } + + public function testSoftDeletedObjectIsRemovedPostFlushWhenEnabled(): void + { + $this->softDeleteableListener->setHandlePostFlushEvent(true); + + $repo = $this->em->getRepository(Article::class); + $commentRepo = $this->em->getRepository(Comment::class); $comment = new Comment(); - $commentField = 'comment'; $commentValue = 'Comment 1'; $comment->setComment($commentValue); + $art0 = new Article(); $field = 'title'; $value = 'Title 1'; @@ -359,27 +543,153 @@ public function testPostSoftDeleteEventIsDispatched() $this->em->persist($art0); $this->em->flush(); - $art = $repo->findOneBy(array($field => $value)); + $art = $repo->findOneBy([$field => $value]); - $this->assertNull($art->getDeletedAt()); - $this->assertNull($comment->getDeletedAt()); + static::assertNull($art->getDeletedAt()); + static::assertNull($comment->getDeletedAt()); + static::assertCount(1, $art->getComments()); + + $this->em->remove($comment); + + // The Comment has been marked for removal, but not yet flushed. This means the + // Comment should still be available. + static::assertInstanceOf(Comment::class, $commentRepo->find($comment->getId())); - $this->em->remove($art); $this->em->flush(); + + // Now that we've flushed, the Comment should no longer be available and should return null. + static::assertNull($commentRepo->find($comment->getId())); } - protected function getUsedEntityFixtures() + public function testSoftDeletedEntityIsNotReinsertedPostFlushWhenDisabled(): void { - return array( - self::ARTICLE_CLASS, - self::PAGE_CLASS, - self::MEGA_PAGE_CLASS, - self::MODULE_CLASS, - self::COMMENT_CLASS, - self::USER_CLASS, - self::OTHER_ARTICLE_CLASS, - self::OTHER_COMMENT_CLASS, - self::MAPPED_SUPERCLASS_CHILD_CLASS, - ); + $authorRepo = $this->em->getRepository(Author::class); + + $author = new Author(); + $firstname = 'first_name'; + $author->setFirstname($firstname); + $lastname = 'last_name'; + $author->setLastname($lastname); + + $article = new Article(); + $title = 'Title 1'; + $article->setTitle($title); + $article->setAuthor($author); + + $this->em->persist($article); + $this->em->flush(); + + $this->em->clear(); + + $author = $authorRepo->findOneBy(['firstname' => $firstname]); + $article = $author->getArticles()[0]; + + static::assertSame($lastname, $author->getLastname()); + static::assertNull($author->getDeletedAt()); + static::assertSame($title, $article->getTitle()); + + $this->em->remove($author); + $this->em->flush(); + + // Flush again + $this->em->flush(); + + // Check whether the entity was re-inserted + $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); + static::assertSame(1, $authorRepo->count(['firstname' => $firstname])); + } + + public function testPostSoftDeleteEventIsDispatched(): void + { + $this->em->getEventManager()->addEventSubscriber(new WithPreAndPostSoftDeleteEventArgsTypeListener()); + + $this->doTestPostSoftDeleteEventIsDispatched(); + } + + /** @group legacy */ + public function testPostSoftDeleteEventIsDispatchedWithDeprecatedListeners(): void + { + $this->em->getEventManager()->addEventSubscriber(new WithoutTypeListener()); + + if (class_exists(LifecycleEventArgs::class)) { + $this->em->getEventManager()->addEventSubscriber(new WithLifecycleEventArgsFromORMTypeListener()); + } + + $this->doTestPostSoftDeleteEventIsDispatched(); + } + + public function testShouldNotDeleteIfColumnNameDifferFromPropertyName(): void + { + $repo = $this->em->getRepository(UserNoHardDelete::class); + + $newUser = new UserNoHardDelete(); + $username = 'test_user'; + $newUser->setUsername($username); + + $this->em->persist($newUser); + $this->em->flush(); + + $user = $repo->findOneBy(['username' => $username]); + + static::assertNull($user->getDeletedAt()); + + $this->em->remove($user); + $this->em->flush(); + + $user = $repo->findOneBy(['username' => $username]); + static::assertNull($user, 'User should be filtered out'); + + // now deactivate filter and attempt to hard delete + $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); + $user = $repo->findOneBy(['username' => $username]); + static::assertNotNull($user, 'User should be fetched when filter is disabled'); + + $this->em->remove($user); + $this->em->flush(); + + $user = $repo->findOneBy(['username' => $username]); + static::assertNotNull($user, 'User is still available, hard delete done'); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + Author::class, + Page::class, + MegaPage::class, + Module::class, + Comment::class, + User::class, + OtherArticle::class, + OtherComment::class, + Child::class, + UserNoHardDelete::class, + ]; + } + + private function doTestPostSoftDeleteEventIsDispatched(): void + { + $repo = $this->em->getRepository(Article::class); + + $comment = new Comment(); + $commentValue = 'Comment 1'; + $comment->setComment($commentValue); + $art0 = new Article(); + $field = 'title'; + $value = 'Title 1'; + $art0->setTitle($value); + $art0->addComment($comment); + + $this->em->persist($art0); + $this->em->flush(); + + $art = $repo->findOneBy([$field => $value]); + + static::assertNull($art->getDeletedAt()); + static::assertNull($comment->getDeletedAt()); + + $this->em->remove($art); + $this->em->flush(); } } diff --git a/tests/Gedmo/Sortable/Fixture/AbstractNode.php b/tests/Gedmo/Sortable/Fixture/AbstractNode.php new file mode 100644 index 0000000000..66dc4136f7 --- /dev/null +++ b/tests/Gedmo/Sortable/Fixture/AbstractNode.php @@ -0,0 +1,100 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\MappedSuperclass + */ +#[ORM\MappedSuperclass] +class AbstractNode +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + protected $id; + + /** + * @var string|null + * + * @ORM\Column(type="string", length=191) + */ + #[ORM\Column(type: Types::STRING, length: 191)] + protected $name; + + /** + * @var string|null + * + * @Gedmo\SortableGroup + * + * @ORM\Column(type="string", length=191) + */ + #[Gedmo\SortableGroup] + #[ORM\Column(type: Types::STRING, length: 191)] + protected $path; + + /** + * @var int|null + * + * @Gedmo\SortablePosition + * + * @ORM\Column(type="integer") + */ + #[Gedmo\SortablePosition] + #[ORM\Column(type: Types::INTEGER)] + protected $position; + + public function getId(): ?int + { + return $this->id; + } + + public function setName(?string $name): void + { + $this->name = $name; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setPath(?string $path): void + { + $this->path = $path; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function setPosition(?int $position): void + { + $this->position = $position; + } + + public function getPosition(): ?int + { + return $this->position; + } +} diff --git a/tests/Gedmo/Sortable/Fixture/Author.php b/tests/Gedmo/Sortable/Fixture/Author.php index fc4d38a117..4e3722d760 100644 --- a/tests/Gedmo/Sortable/Fixture/Author.php +++ b/tests/Gedmo/Sortable/Fixture/Author.php @@ -1,64 +1,94 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sortable\Entity\Repository\SortableRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository") */ +#[ORM\Entity(repositoryClass: SortableRepository::class)] class Author { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="name", type="string") */ - private $name; + #[ORM\Column(name: 'name', type: Types::STRING)] + private ?string $name = null; /** * @Gedmo\SortableGroup + * * @ORM\ManyToOne(targetEntity="Paper", inversedBy="authors") */ - private $paper; + #[Gedmo\SortableGroup] + #[ORM\ManyToOne(targetEntity: Paper::class, inversedBy: 'authors')] + private ?Paper $paper = null; /** * @Gedmo\SortablePosition + * * @ORM\Column(name="position", type="integer") */ - private $position; + #[Gedmo\SortablePosition] + #[ORM\Column(name: 'position', type: Types::INTEGER)] + private ?int $position = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function getName() + + public function getName(): ?string { return $this->name; } - public function setName($name) + + public function setName(?string $name): void { $this->name = $name; } - public function getPaper() + + public function getPaper(): ?Paper { return $this->paper; } - public function setPaper($paper) + + public function setPaper(?Paper $paper): void { $this->paper = $paper; } - public function getPosition() + + public function getPosition(): ?int { return $this->position; } - public function setPosition($position) + + public function setPosition(?int $position): void { $this->position = $position; } diff --git a/tests/Gedmo/Sortable/Fixture/Category.php b/tests/Gedmo/Sortable/Fixture/Category.php index 7a6dda9a2b..bc46eb5ca5 100644 --- a/tests/Gedmo/Sortable/Fixture/Category.php +++ b/tests/Gedmo/Sortable/Fixture/Category.php @@ -1,48 +1,69 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; -use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Category { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="string", length=191) */ - private $name; + #[ORM\Column(type: Types::STRING, length: 191)] + private ?string $name = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Item", mappedBy="category") */ - private $items; + #[ORM\OneToMany(mappedBy: 'category', targetEntity: Item::class)] + private Collection $items; public function __construct() { $this->items = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } diff --git a/tests/Gedmo/Sortable/Fixture/Customer.php b/tests/Gedmo/Sortable/Fixture/Customer.php index dce91d33cf..c5969a885d 100644 --- a/tests/Gedmo/Sortable/Fixture/Customer.php +++ b/tests/Gedmo/Sortable/Fixture/Customer.php @@ -1,53 +1,70 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Sortable\Fixture\CustomerType; /** * @ORM\Entity */ +#[ORM\Entity] class Customer { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="name", type="string") */ - private $name; + #[ORM\Column(name: 'name', type: Types::STRING)] + private ?string $name = null; /** * @ORM\ManyToOne(targetEntity="CustomerType", inversedBy="customers") */ - private $type; + #[ORM\ManyToOne(targetEntity: CustomerType::class, inversedBy: 'customers')] + private ?CustomerType $type = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getType() + public function getType(): ?CustomerType { return $this->type; } - public function setType(CustomerType $type) + public function setType(CustomerType $type): void { $this->type = $type; if (!$type->getCustomers()->contains($this)) { diff --git a/tests/Gedmo/Sortable/Fixture/CustomerType.php b/tests/Gedmo/Sortable/Fixture/CustomerType.php index 40f79dfb98..9492aa443a 100644 --- a/tests/Gedmo/Sortable/Fixture/CustomerType.php +++ b/tests/Gedmo/Sortable/Fixture/CustomerType.php @@ -1,83 +1,112 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Driver\PDO\Exception as PDODriverException; use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Doctrine\Common\Collections\ArrayCollection; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sortable\Entity\Repository\SortableRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository") * @ORM\HasLifecycleCallbacks */ +#[ORM\Entity(repositoryClass: SortableRepository::class)] +#[ORM\HasLifecycleCallbacks] class CustomerType { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="name", type="string") */ - private $name; + #[ORM\Column(name: 'name', type: Types::STRING)] + private ?string $name = null; /** * @Gedmo\SortablePosition + * * @ORM\Column(name="position", type="integer") */ - private $position; + #[Gedmo\SortablePosition] + #[ORM\Column(name: 'position', type: Types::INTEGER)] + private ?int $position = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Customer", mappedBy="type") */ - private $customers; + #[ORM\OneToMany(mappedBy: 'type', targetEntity: Customer::class)] + private Collection $customers; public function __construct() { $this->customers = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getPosition() + public function getPosition(): ?int { return $this->position; } - public function setPosition($position) + public function setPosition(?int $position): void { $this->position = $position; } - public function getCustomers() + /** + * @return Collection + */ + public function getCustomers(): Collection { return $this->customers; } - public function addCustomer(Customer $customer) + public function addCustomer(Customer $customer): void { $this->customers->add($customer); } - public function removeCustomer(Customer $customer) + public function removeCustomer(Customer $customer): void { $this->customers->removeElement($customer); } @@ -85,13 +114,16 @@ public function removeCustomer(Customer $customer) /** * @ORM\PostRemove */ - public function postRemove() + #[ORM\PostRemove] + public function postRemove(): void { if ($this->getCustomers()->count() > 0) { - // we imitate an foreign key constraint exception, because doctrine - // does not support sqlite constraints, which must be tested, too. - $pdoException = new \PDOException('SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails', "23000"); - throw new ForeignKeyConstraintViolationException(sprintf('An exception occurred while deleting the customer type with id %s.', $this->getId()), new PDOException($pdoException)); + // we imitate a foreign key constraint exception because Doctrine + // does not support SQLite constraints, which must be tested, too. + + $pdoException = new \PDOException('SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails', 23000); + + throw new ForeignKeyConstraintViolationException(PDODriverException::new($pdoException), null); } } } diff --git a/tests/Gedmo/Sortable/Fixture/Document/Article.php b/tests/Gedmo/Sortable/Fixture/Document/Article.php index f269e92ea6..82c0015957 100644 --- a/tests/Gedmo/Sortable/Fixture/Document/Article.php +++ b/tests/Gedmo/Sortable/Fixture/Document/Article.php @@ -1,50 +1,72 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { - /** @ODM\Id */ - private $id; + /** + * @var int|null + * + * @Gedmo\SortablePosition + * + * @ODM\Field(type="int") + */ + #[Gedmo\SortablePosition] + #[ODM\Field(type: MongoDBType::INT)] + protected $position; /** - * @ODM\String + * @var string|null + * + * @ODM\Id */ - private $title; + #[ODM\Id] + private $id; /** - * @Gedmo\SortablePosition - * @ODM\Field(type="int") + * @ODM\Field(type="string") */ - protected $position; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setPosition($position) + public function setPosition(?int $position): void { $this->position = $position; } - public function getPosition() + public function getPosition(): ?int { return $this->position; } diff --git a/tests/Gedmo/Sortable/Fixture/Document/Category.php b/tests/Gedmo/Sortable/Fixture/Document/Category.php index 634c8e176d..ee803eb0a0 100644 --- a/tests/Gedmo/Sortable/Fixture/Document/Category.php +++ b/tests/Gedmo/Sortable/Fixture/Document/Category.php @@ -1,35 +1,50 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; -use Gedmo\Mapping\Annotation as Gedmo; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="categories") */ +#[ODM\Document(collection: 'categories')] class Category { - /** @ODM\Id */ + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $name; - + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $name = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } diff --git a/tests/Gedmo/Sortable/Fixture/Document/Kid.php b/tests/Gedmo/Sortable/Fixture/Document/Kid.php index 5787fc6db6..a74ad221e0 100644 --- a/tests/Gedmo/Sortable/Fixture/Document/Kid.php +++ b/tests/Gedmo/Sortable/Fixture/Document/Kid.php @@ -1,66 +1,93 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** * @ODM\Document(collection="kids") */ +#[ODM\Document(collection: 'kids')] class Kid { - /** @ODM\Id */ - private $id; - - /** - * @ODM\String - */ - private $lastname; - /** + * @var int|null + * * @Gedmo\SortablePosition + * * @ODM\Field(type="int") */ + #[Gedmo\SortablePosition] + #[ODM\Field(type: MongoDBType::INT)] protected $position; /** + * @var \DateTime|null + * * @Gedmo\SortableGroup + * * @ODM\Field(type="date") */ + #[Gedmo\SortableGroup] + #[ODM\Field(type: MongoDBType::DATE)] protected $birthdate; - public function getId() + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] + private $id; + + /** + * @ODM\Field(type="string") + */ + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $lastname = null; + + public function getId(): ?string { return $this->id; } - public function setLastname($lastname) + public function setLastname(?string $lastname): void { $this->lastname = $lastname; } - public function getLastname() + public function getLastname(): ?string { return $this->lastname; } - public function setPosition($position) + public function setPosition(?int $position): void { $this->position = $position; } - public function getPosition() + public function getPosition(): ?int { return $this->position; } - public function setBirthdate(\DateTime $birthdate) + public function setBirthdate(\DateTime $birthdate): void { $this->birthdate = $birthdate; } - public function getBirthdate() + public function getBirthdate(): ?\DateTime { return $this->birthdate; } diff --git a/tests/Gedmo/Sortable/Fixture/Document/Post.php b/tests/Gedmo/Sortable/Fixture/Document/Post.php index 816b78256e..aaba8bf972 100644 --- a/tests/Gedmo/Sortable/Fixture/Document/Post.php +++ b/tests/Gedmo/Sortable/Fixture/Document/Post.php @@ -1,67 +1,93 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; -use Sortable\Fixture\Document\Category; /** * @ODM\Document(collection="posts") */ +#[ODM\Document(collection: 'posts')] class Post { - /** @ODM\Id */ - private $id; - - /** - * @ODM\String - */ - private $title; - /** + * @var int|null + * * @Gedmo\SortablePosition + * * @ODM\Field(type="int") */ + #[Gedmo\SortablePosition] + #[ODM\Field(type: MongoDBType::INT)] protected $position; /** + * @var Category|null + * * @Gedmo\SortableGroup - * @ODM\ReferenceOne(targetDocument="Sortable\Fixture\Document\Category") + * + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\Sortable\Fixture\Document\Category") */ + #[Gedmo\SortableGroup] + #[ODM\ReferenceOne(targetDocument: Category::class)] protected $category; - public function getId() + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] + private $id; + + /** + * @ODM\Field(type="string") + */ + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; + + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setPosition($position) + public function setPosition(?int $position): void { $this->position = $position; } - public function getPosition() + public function getPosition(): ?int { return $this->position; } - public function setCategory(Category $category) + public function setCategory(Category $category): void { $this->category = $category; } - public function getCategory() + public function getCategory(): ?Category { return $this->category; } diff --git a/tests/Gedmo/Sortable/Fixture/Event.php b/tests/Gedmo/Sortable/Fixture/Event.php index 712eda6417..9a1481ec97 100644 --- a/tests/Gedmo/Sortable/Fixture/Event.php +++ b/tests/Gedmo/Sortable/Fixture/Event.php @@ -1,13 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sortable\Entity\Repository\SortableRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository") */ +#[ORM\Entity(repositoryClass: SortableRepository::class)] class Event { /** @@ -17,62 +29,66 @@ class Event * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** - * @var \DateTime - * * @Gedmo\SortableGroup + * * @ORM\Column(type="datetime") */ - private $dateTime; + #[Gedmo\SortableGroup] + #[ORM\Column(type: Types::DATETIME_MUTABLE)] + private ?\DateTime $dateTime = null; /** - * @var string - * - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="string", length=191) */ - private $name; + #[ORM\Column(type: Types::STRING, length: 191)] + private ?string $name = null; /** - * @var int - * * @Gedmo\SortablePosition + * * @ORM\Column(type="integer") */ - private $position; + #[Gedmo\SortablePosition] + #[ORM\Column(type: Types::INTEGER)] + private ?int $position = null; - public function getId() + public function getId(): int { return $this->id; } - public function setDateTime(\DateTime $date) + public function setDateTime(\DateTime $date): void { $this->dateTime = $date; } - public function getDateTime() + public function getDateTime(): \DateTime { return $this->dateTime; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): string { return $this->name; } - public function setPosition($position) + public function setPosition(int $position): void { $this->position = $position; } - public function getPosition() + public function getPosition(): int { return $this->position; } diff --git a/tests/Gedmo/Sortable/Fixture/Item.php b/tests/Gedmo/Sortable/Fixture/Item.php index bc31308ee5..50ed86a013 100644 --- a/tests/Gedmo/Sortable/Fixture/Item.php +++ b/tests/Gedmo/Sortable/Fixture/Item.php @@ -1,70 +1,94 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sortable\Entity\Repository\SortableRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository") */ +#[ORM\Entity(repositoryClass: SortableRepository::class)] class Item { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="string", length=191) */ - private $name; + #[ORM\Column(type: Types::STRING, length: 191)] + private ?string $name = null; /** * @Gedmo\SortablePosition + * * @ORM\Column(type="integer") */ - private $position; + #[Gedmo\SortablePosition] + #[ORM\Column(type: Types::INTEGER)] + private ?int $position = null; /** * @Gedmo\SortableGroup + * * @ORM\ManyToOne(targetEntity="Category", inversedBy="items") */ - private $category; + #[Gedmo\SortableGroup] + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'items')] + private ?Category $category = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setPosition($position) + public function setPosition(?int $position): void { $this->position = $position; } - public function getPosition() + public function getPosition(): ?int { return $this->position; } - public function setCategory(Category $category = null) + public function setCategory(?Category $category = null): void { $this->category = $category; } - public function getCategory() + public function getCategory(): ?Category { return $this->category; } diff --git a/tests/Gedmo/Sortable/Fixture/ItemWithDateColumn.php b/tests/Gedmo/Sortable/Fixture/ItemWithDateColumn.php new file mode 100644 index 0000000000..01241d8acf --- /dev/null +++ b/tests/Gedmo/Sortable/Fixture/ItemWithDateColumn.php @@ -0,0 +1,80 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sortable\Entity\Repository\SortableRepository; + +/** + * @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository") + */ +#[ORM\Entity(repositoryClass: SortableRepository::class)] +class ItemWithDateColumn +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; + + /** + * @Gedmo\SortablePosition + * + * @ORM\Column(type="integer") + */ + #[Gedmo\SortablePosition] + #[ORM\Column(type: Types::INTEGER)] + private int $position = 0; + + /** + * @Gedmo\SortableGroup + * + * @ORM\Column(type="date") + */ + #[Gedmo\SortableGroup] + #[ORM\Column(type: Types::DATE_MUTABLE)] + private ?\DateTime $date = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(?int $id): void + { + $this->id = $id; + } + + public function getPosition(): int + { + return $this->position; + } + + public function setPosition(int $position): void + { + $this->position = $position; + } + + public function getDate(): ?\DateTime + { + return $this->date; + } + + public function setDate(?\DateTime $date): void + { + $this->date = $date; + } +} diff --git a/tests/Gedmo/Sortable/Fixture/Node.php b/tests/Gedmo/Sortable/Fixture/Node.php index f1daf2d5dc..62fc3a1a46 100644 --- a/tests/Gedmo/Sortable/Fixture/Node.php +++ b/tests/Gedmo/Sortable/Fixture/Node.php @@ -1,71 +1,23 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; -use Gedmo\Mapping\Annotation as Gedmo; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Sortable\Entity\Repository\SortableRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository") */ -class Node +#[ORM\Entity(repositoryClass: SortableRepository::class)] +class Node extends AbstractNode { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - private $id; - - /** - * @ORM\Column(type="string", length=255) - */ - private $name; - - /** - * @Gedmo\SortableGroup - * @ORM\Column(type="string", length=255) - */ - private $path; - - /** - * @Gedmo\SortablePosition - * @ORM\Column(type="integer") - */ - private $position; - - public function getId() - { - return $this->id; - } - - public function setName($name) - { - $this->name = $name; - } - - public function getName() - { - return $this->name; - } - - public function setPath($path) - { - $this->path = $path; - } - - public function getPath() - { - return $this->path; - } - - public function setPosition($position) - { - $this->position = $position; - } - - public function getPosition() - { - return $this->position; - } } diff --git a/tests/Gedmo/Sortable/Fixture/NotifyNode.php b/tests/Gedmo/Sortable/Fixture/NotifyNode.php new file mode 100644 index 0000000000..dfc63184e8 --- /dev/null +++ b/tests/Gedmo/Sortable/Fixture/NotifyNode.php @@ -0,0 +1,85 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; + +use Doctrine\ORM\Mapping as ORM; +use Doctrine\Persistence\NotifyPropertyChanged; +use Doctrine\Persistence\PropertyChangedListener; +use Gedmo\Sortable\Entity\Repository\SortableRepository; + +/** + * @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository") + * @ORM\ChangeTrackingPolicy("NOTIFY") + */ +#[ORM\Entity(repositoryClass: SortableRepository::class)] +#[ORM\ChangeTrackingPolicy(value: 'NOTIFY')] +class NotifyNode extends AbstractNode implements NotifyPropertyChanged +{ + /** + * Listeners that want to be notified about property changes. + * + * @var PropertyChangedListener[] + */ + private $_propertyChangedListeners = []; + + /** + * Adds a listener that wants to be notified about property changes. + * + * @see \Doctrine\Common\NotifyPropertyChanged::addPropertyChangedListener() + * + * @return void + */ + public function addPropertyChangedListener(PropertyChangedListener $listener) + { + $this->_propertyChangedListeners[] = $listener; + } + + public function setName(?string $name): void + { + $this->setProperty('name', $name); + } + + public function setPath(?string $path): void + { + $this->setProperty('path', $path); + } + + public function setPosition(?int $position): void + { + $this->setProperty('position', $position); + } + + /** + * Notify property change event to listeners + * + * @param mixed $oldValue + * @param mixed $newValue + */ + protected function triggerPropertyChanged(string $propName, $oldValue, $newValue): void + { + foreach ($this->_propertyChangedListeners as $listener) { + $listener->propertyChanged($this, $propName, $oldValue, $newValue); + } + } + + /** + * @param mixed $newValue + */ + protected function setProperty(string $property, $newValue): void + { + $oldValue = $this->{$property}; + if ($oldValue !== $newValue) { + $this->triggerPropertyChanged($property, $oldValue, $newValue); + $this->{$property} = $newValue; + } + } +} diff --git a/tests/Gedmo/Sortable/Fixture/Paper.php b/tests/Gedmo/Sortable/Fixture/Paper.php index 2a372cae54..bb65485252 100644 --- a/tests/Gedmo/Sortable/Fixture/Paper.php +++ b/tests/Gedmo/Sortable/Fixture/Paper.php @@ -1,53 +1,82 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; -use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Paper { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="name", type="string") */ - private $name; + #[ORM\Column(name: 'name', type: Types::STRING)] + private ?string $name = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Author", mappedBy="paper", cascade={"persist", "remove"}) */ - private $authors; + #[ORM\OneToMany(mappedBy: 'paper', targetEntity: Author::class, cascade: ['persist', 'remove'])] + private Collection $authors; public function __construct() { $this->authors = new ArrayCollection(); } - public function getId() + + public function getId(): ?int { return $this->id; } - public function getName() + + public function getName(): ?string { return $this->name; } - public function setName($name) + + public function setName(?string $name): void { $this->name = $name; } - public function getAuthors() + + /** + * @return Collection + */ + public function getAuthors(): Collection { return $this->authors; } - public function addAuthor($author) + + public function addAuthor(Author $author): void { $this->authors->add($author); } diff --git a/tests/Gedmo/Sortable/Fixture/SimpleListItem.php b/tests/Gedmo/Sortable/Fixture/SimpleListItem.php index 5ee4e5db5c..8ec6e67242 100644 --- a/tests/Gedmo/Sortable/Fixture/SimpleListItem.php +++ b/tests/Gedmo/Sortable/Fixture/SimpleListItem.php @@ -1,54 +1,75 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Sortable\Entity\Repository\SortableRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository") */ +#[ORM\Entity(repositoryClass: SortableRepository::class)] class SimpleListItem { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="string", length=191) */ - private $name; + #[ORM\Column(type: Types::STRING, length: 191)] + private ?string $name = null; /** * @Gedmo\SortablePosition + * * @ORM\Column(type="integer") */ - private $position; + #[Gedmo\SortablePosition] + #[ORM\Column(type: Types::INTEGER)] + private ?int $position = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setPosition($position) + public function setPosition(?int $position): void { $this->position = $position; } - public function getPosition() + public function getPosition(): ?int { return $this->position; } diff --git a/tests/Gedmo/Sortable/Fixture/Transport/Bus.php b/tests/Gedmo/Sortable/Fixture/Transport/Bus.php index 2cdbc7585b..940f12a2fc 100644 --- a/tests/Gedmo/Sortable/Fixture/Transport/Bus.php +++ b/tests/Gedmo/Sortable/Fixture/Transport/Bus.php @@ -1,12 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture\Transport; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Bus extends Vehicle { } diff --git a/tests/Gedmo/Sortable/Fixture/Transport/Car.php b/tests/Gedmo/Sortable/Fixture/Transport/Car.php index 78d9ebfc73..378785a7cd 100644 --- a/tests/Gedmo/Sortable/Fixture/Transport/Car.php +++ b/tests/Gedmo/Sortable/Fixture/Transport/Car.php @@ -1,36 +1,61 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture\Transport; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Car extends Vehicle { /** * @ORM\ManyToOne(targetEntity="Car", inversedBy="children") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + private ?Car $parent = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Car", mappedBy="parent") */ - private $children; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + private Collection $children; + + public function __construct() + { + $this->children = new ArrayCollection(); + } - public function setParent($parent = null) + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getChildren() + /** + * @return Collection + */ + public function getChildren(): Collection { return $this->children; } - public function getParent() + public function getParent(): ?self { return $this->parent; } diff --git a/tests/Gedmo/Sortable/Fixture/Transport/Engine.php b/tests/Gedmo/Sortable/Fixture/Transport/Engine.php index 671677f5c1..d8407f2b56 100644 --- a/tests/Gedmo/Sortable/Fixture/Transport/Engine.php +++ b/tests/Gedmo/Sortable/Fixture/Transport/Engine.php @@ -1,52 +1,70 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture\Transport; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Engine { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=32) */ - private $type; + #[ORM\Column(length: 32)] + private ?string $type = null; /** * @ORM\Column(type="integer") */ - private $valves; + #[ORM\Column(type: Types::INTEGER)] + private ?int $valves = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setType($type) + public function setType(?string $type): void { $this->type = $type; } - public function getType() + public function getType(): ?string { return $this->type; } - public function setValves($valves) + public function setValves(?int $valves): void { $this->valves = $valves; } - public function getValves() + public function getValves(): ?int { return $this->valves; } diff --git a/tests/Gedmo/Sortable/Fixture/Transport/Reservation.php b/tests/Gedmo/Sortable/Fixture/Transport/Reservation.php index 2d5abb6570..2c0ac1823a 100644 --- a/tests/Gedmo/Sortable/Fixture/Transport/Reservation.php +++ b/tests/Gedmo/Sortable/Fixture/Transport/Reservation.php @@ -1,103 +1,130 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture\Transport; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Reservation { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\ManyToOne(targetEntity="Bus") */ - private $bus; + #[ORM\ManyToOne(targetEntity: Bus::class)] + private ?Bus $bus = null; /** * Bus destination * * @Gedmo\SortableGroup - * @ORM\Column(length=255) + * + * @ORM\Column(length=191) */ - private $destination; + #[Gedmo\SortableGroup] + #[ORM\Column(length: 191)] + private ?string $destination = null; /** * @Gedmo\SortableGroup + * * @ORM\Column(type="datetime") */ - private $travelDate; + #[Gedmo\SortableGroup] + #[ORM\Column(type: Types::DATETIME_MUTABLE)] + private ?\DateTime $travelDate = null; /** * @Gedmo\SortablePosition + * * @ORM\Column(type="integer") */ - private $seat; + #[Gedmo\SortablePosition] + #[ORM\Column(type: Types::INTEGER)] + private ?int $seat = null; /** - * @ORM\Column(length=255) + * @ORM\Column(length=191) */ - private $name; + #[ORM\Column(length: 191)] + private ?string $name = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setBus(Bus $bus) + public function setBus(Bus $bus): void { $this->bus = $bus; } - public function getBus() + public function getBus(): ?Bus { return $this->bus; } - public function setDestination($destination) + public function setDestination(?string $destination): void { $this->destination = $destination; } - public function getDestination() + public function getDestination(): ?string { return $this->destination; } - public function setTravelDate(\DateTime $date) + public function setTravelDate(\DateTime $date): void { $this->travelDate = $date; } - public function getTravelDate() + public function getTravelDate(): ?\DateTime { return $this->travelDate; } - public function setSeat($seat) + public function setSeat(?int $seat): void { $this->seat = $seat; } - public function getSeat() + public function getSeat(): ?int { return $this->seat; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } diff --git a/tests/Gedmo/Sortable/Fixture/Transport/Vehicle.php b/tests/Gedmo/Sortable/Fixture/Transport/Vehicle.php index bbdcca5667..7b6d789f62 100644 --- a/tests/Gedmo/Sortable/Fixture/Transport/Vehicle.php +++ b/tests/Gedmo/Sortable/Fixture/Transport/Vehicle.php @@ -1,77 +1,103 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable\Fixture\Transport; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="discriminator", type="string") * @ORM\DiscriminatorMap({ - * "vehicle" = "Vehicle", - * "car" = "Car", - * "bus" = "Bus" + * "vehicle": "Vehicle", + * "car": "Car", + * "bus": "Bus" * }) */ +#[ORM\Entity] +#[ORM\InheritanceType('JOINED')] +#[ORM\DiscriminatorColumn(name: 'discriminator', type: Types::STRING)] +#[ORM\DiscriminatorMap(['vehicle' => Vehicle::class, 'car' => Car::class, 'bus' => Bus::class])] class Vehicle { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\SortableGroup + * * @ORM\ManyToOne(targetEntity="Engine") */ - private $engine; + #[Gedmo\SortableGroup] + #[ORM\ManyToOne(targetEntity: Engine::class)] + private ?Engine $engine = null; /** * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(length: 128)] + private ?string $title = null; /** * @Gedmo\SortablePosition + * * @ORM\Column(type="integer") */ - private $sortByEngine; + #[Gedmo\SortablePosition] + #[ORM\Column(type: Types::INTEGER)] + private ?int $sortByEngine = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setSortByEngine($sort) + public function setSortByEngine(?int $sort): void { $this->sortByEngine = $sort; } - public function getSortByEngine() + public function getSortByEngine(): ?int { return $this->sortByEngine; } - public function setEngine(Engine $engine) + public function setEngine(Engine $engine): void { $this->engine = $engine; } - public function getEngine() + public function getEngine(): ?Engine { return $this->engine; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Sortable/SortableDocumentGroupTest.php b/tests/Gedmo/Sortable/SortableDocumentGroupTest.php index fdc18d637a..a84846d770 100644 --- a/tests/Gedmo/Sortable/SortableDocumentGroupTest.php +++ b/tests/Gedmo/Sortable/SortableDocumentGroupTest.php @@ -1,28 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Sortable\Fixture\Document\Post; -use Sortable\Fixture\Document\Category; -use Sortable\Fixture\Document\Kid; +use Gedmo\Sortable\SortableListener; +use Gedmo\Tests\Sortable\Fixture\Document\Category; +use Gedmo\Tests\Sortable\Fixture\Document\Kid; +use Gedmo\Tests\Sortable\Fixture\Document\Post; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for sortable behavior with SortableGroup * * @author http://github.com/vetalt - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SortableDocumentGroupTest extends BaseTestCaseMongoODM +final class SortableDocumentGroupTest extends BaseTestCaseMongoODM { - const POST = 'Sortable\\Fixture\\Document\\Post'; - const CATEGORY = 'Sortable\\Fixture\\Document\\Category'; - const KID = 'Sortable\\Fixture\\Document\\Kid'; - const KID_DATE1 = '1999-12-31'; - const KID_DATE2 = '2000-01-01'; + private const KID_DATE1 = '1999-12-31'; + private const KID_DATE2 = '2000-01-01'; - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); @@ -32,149 +38,149 @@ protected function setUp() $this->populate(); } - /** - * Insert 2 categories, 6 posts and 4 kids - * 3 posts are linked to a category, and 3 to the other one - * 2 kids have one date, 2 another one - */ - private function populate() - { - $categories = array(); - for ($i = 0; $i < 2; $i++) { - $categories[$i] = new Category(); - $categories[$i]->setName('category'.$i); - $this->dm->persist($categories[$i]); - } - - for ($i = 0; $i < 6; $i++) { - $post = new Post(); - $post->setTitle('post'.$i); - $post->setCategory($categories[($i % 2)]); - $this->dm->persist($post); - } - - $birthdates = array( - new \DateTime(self::KID_DATE1), - new \DateTime(self::KID_DATE2), - ); - - for ($i = 0; $i < 4; $i++) { - $kid = new Kid(); - $kid->setLastName('kid'.$i); - $kid->setBirthdate($birthdates[($i % 2)]); - $this->dm->persist($kid); - } - $this->dm->flush(); - $this->dm->clear(); - } - /** * There should be 2 kids by position */ - public function testKidInitialPositions() + public function testKidInitialPositions(): void { - $repo = $this->dm->getRepository(self::KID); + $repo = $this->dm->getRepository(Kid::class); - for ($i = 0; $i < 2; $i++) { - $kids = $repo->findByPosition($i); - $this->assertCount(2, $kids); + for ($i = 0; $i < 2; ++$i) { + $kids = $repo->findBy(['position' => $i]); + static::assertCount(2, $kids); } } /** * Move the last kid in the first position */ - public function testKidMovePosition() + public function testKidMovePosition(): void { - $repo = $this->dm->getRepository(self::KID); + $repo = $this->dm->getRepository(Kid::class); - $kid = $repo->findOneByLastname('kid2'); - $this->assertInstanceOf(self::KID, $kid); + $kid = $repo->findOneBy(['lastname' => 'kid2']); + static::assertInstanceOf(Kid::class, $kid); $kid->setPosition(0); $this->dm->flush(); - $kids = $repo->findByBirthdate(new \DateTime(self::KID_DATE1)); - $this->assertCount(2, $kids); + $kids = $repo->findBy(['birthdate' => new \DateTime(self::KID_DATE1)]); + static::assertCount(2, $kids); - for ($i=0; $i < 2; $i++) { - $expected = ($i+1 == 1) ? $i+1 : 0; - $this->assertEquals($expected, $kids[$i]->getPosition()); + for ($i = 0; $i < 2; ++$i) { + $expected = (1 == $i + 1) ? $i + 1 : 0; + static::assertSame($expected, $kids[$i]->getPosition()); } } /** * There should be 2 posts by position */ - public function testPostsInitialPositions() + public function testPostsInitialPositions(): void { - $repo = $this->dm->getRepository(self::POST); + $repo = $this->dm->getRepository(Post::class); - for ($i = 0; $i < 3; $i++) { - $posts = $repo->findByPosition($i); - $this->assertCount(2, $posts); + for ($i = 0; $i < 3; ++$i) { + $posts = $repo->findBy(['position' => $i]); + static::assertCount(2, $posts); } } /** * Move the last inserted post in first position and check */ - public function testPostsMovePosition() + public function testPostsMovePosition(): void { - $repo_category = $this->dm->getRepository(self::CATEGORY); - $repo_post = $this->dm->getRepository(self::POST); + $repo_category = $this->dm->getRepository(Category::class); + $repo_post = $this->dm->getRepository(Post::class); - $category = $repo_category->findOneByName('category1'); - $this->assertInstanceOf(self::CATEGORY, $category); + $category = $repo_category->findOneBy(['name' => 'category1']); + static::assertInstanceOf(Category::class, $category); - $post = $repo_post->findOneBy(array( + $post = $repo_post->findOneBy([ 'position' => 2, - 'category.id' => $category->getId() - )); - $this->assertInstanceOf(self::POST, $post); + 'category.id' => $category->getId(), + ]); + static::assertInstanceOf(Post::class, $post); $post->setPosition(0); $this->dm->flush(); - $posts = $repo_post->findBy(array( - 'category.id' => $category->getId() - )); - $this->assertCount(3, $posts); - - for ($i=0; $i < 3; $i++) { - $expected = ($i+1 < 3) ? $i+1 : 0; - $this->assertEquals($expected, $posts[$i]->getPosition()); + $posts = $repo_post->findBy([ + 'category.id' => $category->getId(), + ]); + static::assertCount(3, $posts); + + for ($i = 0; $i < 3; ++$i) { + $expected = ($i + 1 < 3) ? $i + 1 : 0; + static::assertSame($expected, $posts[$i]->getPosition()); } } /** * Delete the 2nd post linked to a Category and check */ - public function testPostsDeletePosition() + public function testPostsDeletePosition(): void { - $repo_category = $this->dm->getRepository(self::CATEGORY); - $repo_post = $this->dm->getRepository(self::POST); + $repo_category = $this->dm->getRepository(Category::class); + $repo_post = $this->dm->getRepository(Post::class); - $category = $repo_category->findOneByName('category1'); - $this->assertInstanceOf(self::CATEGORY, $category); + $category = $repo_category->findOneBy(['name' => 'category1']); + static::assertInstanceOf(Category::class, $category); - $post = $repo_post->findOneBy(array( + $post = $repo_post->findOneBy([ 'position' => 1, - 'category.id' => $category->getId() - )); - $this->assertInstanceOf(self::POST, $post); + 'category.id' => $category->getId(), + ]); + static::assertInstanceOf(Post::class, $post); $this->dm->remove($post); $this->dm->flush(); - $posts = $repo_post->findBy(array( - 'category.id' => $category->getId() - )); - $this->assertCount(2, $posts); - - for ($i=0; $i < 2; $i++) { - $this->assertEquals($i, $posts[$i]->getPosition()); + $posts = $repo_post->findBy([ + 'category.id' => $category->getId(), + ]); + static::assertCount(2, $posts); + + for ($i = 0; $i < 2; ++$i) { + static::assertSame($i, $posts[$i]->getPosition()); + } + } + + /** + * Insert 2 categories, 6 posts and 4 kids + * 3 posts are linked to a category, and 3 to the other one + * 2 kids have one date, 2 another one + */ + private function populate(): void + { + $categories = []; + for ($i = 0; $i < 2; ++$i) { + $categories[$i] = new Category(); + $categories[$i]->setName('category'.$i); + $this->dm->persist($categories[$i]); + } + + for ($i = 0; $i < 6; ++$i) { + $post = new Post(); + $post->setTitle('post'.$i); + $post->setCategory($categories[$i % 2]); + $this->dm->persist($post); + } + + $birthdates = [ + new \DateTime(self::KID_DATE1), + new \DateTime(self::KID_DATE2), + ]; + + for ($i = 0; $i < 4; ++$i) { + $kid = new Kid(); + $kid->setLastname('kid'.$i); + $kid->setBirthdate($birthdates[$i % 2]); + $this->dm->persist($kid); } + $this->dm->flush(); + $this->dm->clear(); } } diff --git a/tests/Gedmo/Sortable/SortableDocumentTest.php b/tests/Gedmo/Sortable/SortableDocumentTest.php index fb5c16f437..7e528e2fc6 100644 --- a/tests/Gedmo/Sortable/SortableDocumentTest.php +++ b/tests/Gedmo/Sortable/SortableDocumentTest.php @@ -1,22 +1,29 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Sortable\Fixture\Document\Article; +use Gedmo\Sortable\SortableListener; +use Gedmo\Tests\Sortable\Fixture\Document\Article; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; /** * These are tests for sortable behavior * * @author http://github.com/vetalt - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SortableDocumentTest extends BaseTestCaseMongoODM +final class SortableDocumentTest extends BaseTestCaseMongoODM { - const ARTICLE = 'Sortable\\Fixture\\Document\\Article'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); @@ -26,67 +33,67 @@ protected function setUp() $this->populate(); } - private function populate() - { - for ($i = 0; $i <= 4; $i++) { - $article = new Article(); - $article->setTitle('article'.$i); - $this->dm->persist($article); - } - $this->dm->flush(); - $this->dm->clear(); - } - - public function testInitialPositions() + public function testInitialPositions(): void { - $repo = $this->dm->getRepository(self::ARTICLE); - for ($i = 0; $i <= 4; $i++) { - $article = $repo->findOneByPosition($i); - $this->assertEquals('article'.$i, $article->getTitle()); + $repo = $this->dm->getRepository(Article::class); + for ($i = 0; $i <= 4; ++$i) { + $article = $repo->findOneBy(['position' => $i]); + static::assertSame('article'.$i, $article->getTitle()); } } - public function testMovePositions() + public function testMovePositions(): void { - $repo = $this->dm->getRepository(self::ARTICLE); + $repo = $this->dm->getRepository(Article::class); - $article = $repo->findOneByPosition(4); + $article = $repo->findOneBy(['position' => 4]); $article->setPosition(0); $this->dm->flush(); - for ($i = 1; $i <= 4; $i++) { - $article = $repo->findOneByPosition($i); - $this->assertEquals('article'.($i-1), $article->getTitle()); + for ($i = 1; $i <= 4; ++$i) { + $article = $repo->findOneBy(['position' => $i]); + static::assertSame('article'.($i - 1), $article->getTitle()); } } - public function testMoveLastPositions() + public function testMoveLastPositions(): void { - $repo = $this->dm->getRepository(self::ARTICLE); + $repo = $this->dm->getRepository(Article::class); - $article = $repo->findOneByPosition(0); + $article = $repo->findOneBy(['position' => 0]); $article->setPosition(-1); $this->dm->flush(); - for ($i = 0; $i <= 3; $i++) { - $article = $repo->findOneByPosition($i); - $this->assertEquals('article'.($i+1), $article->getTitle()); + for ($i = 0; $i <= 3; ++$i) { + $article = $repo->findOneBy(['position' => $i]); + static::assertSame('article'.($i + 1), $article->getTitle()); } - $article = $repo->findOneByPosition(4); - $this->assertEquals('article0', $article->getTitle()); + $article = $repo->findOneBy(['position' => 4]); + static::assertSame('article0', $article->getTitle()); } - public function testDeletePositions() + public function testDeletePositions(): void { - $repo = $this->dm->getRepository(self::ARTICLE); + $repo = $this->dm->getRepository(Article::class); - $article = $repo->findOneByPosition(0); + $article = $repo->findOneBy(['position' => 0]); $this->dm->remove($article); $this->dm->flush(); - for ($i = 0; $i <= 3; $i++) { - $article = $repo->findOneByPosition($i); - $this->assertEquals('article'.($i+1), $article->getTitle()); + for ($i = 0; $i <= 3; ++$i) { + $article = $repo->findOneBy(['position' => $i]); + static::assertSame('article'.($i + 1), $article->getTitle()); } } + + private function populate(): void + { + for ($i = 0; $i <= 4; ++$i) { + $article = new Article(); + $article->setTitle('article'.$i); + $this->dm->persist($article); + } + $this->dm->flush(); + $this->dm->clear(); + } } diff --git a/tests/Gedmo/Sortable/SortableGroupTest.php b/tests/Gedmo/Sortable/SortableGroupTest.php index 993bb1ecfd..8b928b5727 100644 --- a/tests/Gedmo/Sortable/SortableGroupTest.php +++ b/tests/Gedmo/Sortable/SortableGroupTest.php @@ -1,265 +1,280 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable; use Doctrine\Common\EventManager; -use Sortable\Fixture\Category; -use Sortable\Fixture\Item; -use Tool\BaseTestCaseORM; -use Sortable\Fixture\Transport\Car; -use Sortable\Fixture\Transport\Bus; -use Sortable\Fixture\Transport\Vehicle; -use Sortable\Fixture\Transport\Engine; -use Sortable\Fixture\Transport\Reservation; +use Gedmo\Sortable\SortableListener; +use Gedmo\Tests\Sortable\Fixture\Category; +use Gedmo\Tests\Sortable\Fixture\Item; +use Gedmo\Tests\Sortable\Fixture\ItemWithDateColumn; +use Gedmo\Tests\Sortable\Fixture\Transport\Bus; +use Gedmo\Tests\Sortable\Fixture\Transport\Car; +use Gedmo\Tests\Sortable\Fixture\Transport\Engine; +use Gedmo\Tests\Sortable\Fixture\Transport\Reservation; +use Gedmo\Tests\Sortable\Fixture\Transport\Vehicle; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for sluggable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SortableGroupTest extends BaseTestCaseORM +final class SortableGroupTest extends BaseTestCaseORM { - const CAR = "Sortable\Fixture\Transport\Car"; - const BUS = "Sortable\Fixture\Transport\Bus"; - const VEHICLE = "Sortable\Fixture\Transport\Vehicle"; - const ENGINE = "Sortable\Fixture\Transport\Engine"; - const RESERVATION = "Sortable\Fixture\Transport\Reservation"; - const ITEM = "Sortable\Fixture\Item"; - const CATEGORY = "Sortable\Fixture\Category"; - - const SEATS = 3; + private const SEATS = 3; - const TRAVEL_DATE_FORMAT = 'Y-m-d H:i'; - const TODAY = '2013-10-24 12:50'; - const TOMORROW = '2013-10-25 12:50'; + private const TRAVEL_DATE_FORMAT = 'Y-m-d H:i'; + private const TODAY = '2013-10-24 12:50'; + private const TOMORROW = '2013-10-25 12:50'; - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SortableListener()); - $this->getMockSqliteEntityManager($evm); - /*$this->getMockCustomEntityManager(array( - 'driver' => 'pdo_mysql', - 'dbname' => 'test', - 'host' => '127.0.0.1', - 'user' => 'root', - 'password' => 'nimda' - ), $evm);*/ + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldBeAbleToRemove() + public function testShouldBeAbleToRemove(): void { $this->populate(); - $carRepo = $this->em->getRepository(self::CAR); + $carRepo = $this->em->getRepository(Car::class); - $audi80 = $carRepo->findOneByTitle('Audi-80'); - $this->assertEquals(0, $audi80->getSortByEngine()); + $audi80 = $carRepo->findOneBy(['title' => 'Audi-80']); + static::assertSame(0, $audi80->getSortByEngine()); - $audi80s = $carRepo->findOneByTitle('Audi-80s'); - $this->assertEquals(1, $audi80s->getSortByEngine()); + $audi80s = $carRepo->findOneBy(['title' => 'Audi-80s']); + static::assertSame(1, $audi80s->getSortByEngine()); - $icarus = $this->em->getRepository(self::BUS)->findOneByTitle('Icarus'); - $this->assertEquals(2, $icarus->getSortByEngine()); + $icarus = $this->em->getRepository(Bus::class)->findOneBy(['title' => 'Icarus']); + static::assertSame(2, $icarus->getSortByEngine()); $this->em->remove($audi80); $this->em->flush(); - $audi80s = $carRepo->findOneByTitle('Audi-80s'); - $this->assertEquals(0, $audi80s->getSortByEngine()); + $audi80s = $carRepo->findOneBy(['title' => 'Audi-80s']); + static::assertSame(0, $audi80s->getSortByEngine()); - $icarus = $this->em->getRepository(self::BUS)->findOneByTitle('Icarus'); - $this->assertEquals(1, $icarus->getSortByEngine()); + $icarus = $this->em->getRepository(Bus::class)->findOneBy(['title' => 'Icarus']); + static::assertSame(1, $icarus->getSortByEngine()); } /** - * @test * fix issue #502 */ - public function shouldBeAbleToChangeGroup() + public function testShouldBeAbleToChangeGroup(): void { $this->populate(); - $carRepo = $this->em->getRepository(self::CAR); + $carRepo = $this->em->getRepository(Car::class); // position 0 - $audi80 = $carRepo->findOneByTitle('Audi-80'); - $this->assertEquals(0, $audi80->getSortByEngine()); + $audi80 = $carRepo->findOneBy(['title' => 'Audi-80']); + static::assertSame(0, $audi80->getSortByEngine()); - //position 1 - $audi80s = $carRepo->findOneByTitle('Audi-80s'); - $this->assertEquals(1, $audi80s->getSortByEngine()); + // position 1 + $audi80s = $carRepo->findOneBy(['title' => 'Audi-80s']); + static::assertSame(1, $audi80s->getSortByEngine()); - //position 2 - $icarus = $this->em->getRepository(self::BUS)->findOneByTitle('Icarus'); - $this->assertEquals(2, $icarus->getSortByEngine()); + // position 2 + $icarus = $this->em->getRepository(Bus::class)->findOneBy(['title' => 'Icarus']); + static::assertSame(2, $icarus->getSortByEngine()); // theres only 1 v6 so this should be position:0 - $audiJet = $carRepo->findOneByTitle('Audi-jet'); - $this->assertEquals(0, $audiJet->getSortByEngine()); + $audiJet = $carRepo->findOneBy(['title' => 'Audi-jet']); + static::assertSame(0, $audiJet->getSortByEngine()); // change engines - $v6engine = $this->em->getRepository(self::ENGINE)->findOneByType('V6'); + $v6engine = $this->em->getRepository(Engine::class)->findOneBy(['type' => 'V6']); $audi80s->setEngine($v6engine); $this->em->flush(); // v6 - $this->assertEquals(0, $audiJet->getSortByEngine()); - $this->assertEquals(1, $audi80s->getSortByEngine()); + static::assertSame(0, $audiJet->getSortByEngine()); + static::assertSame(1, $audi80s->getSortByEngine()); // v8 - $this->assertEquals(0, $audi80->getSortByEngine()); - $this->assertEquals(1, $icarus->getSortByEngine()); + static::assertSame(0, $audi80->getSortByEngine()); + static::assertSame(1, $icarus->getSortByEngine()); } /** - * @test * issue #873 */ - public function shouldBeAbleToChangeGroupWhenMultiGroups() + public function testShouldBeAbleToChangeGroupWhenMultiGroups(): void { $this->populate(); - $repo = $this->em->getRepository(self::RESERVATION); + $repo = $this->em->getRepository(Reservation::class); $today = \DateTime::createFromFormat(self::TRAVEL_DATE_FORMAT, self::TODAY); $tomorrow = \DateTime::createFromFormat(self::TRAVEL_DATE_FORMAT, self::TOMORROW); - for ($i = 0; $i < self::SEATS; $i++) { - $reservation = $repo->findOneByName('Bratislava Today '.$i); - $this->assertNotNull($reservation); - $this->assertEquals($i, $reservation->getSeat()); + for ($i = 0; $i < self::SEATS; ++$i) { + $reservation = $repo->findOneBy(['name' => 'Bratislava Today '.$i]); + static::assertNotNull($reservation); + static::assertSame($i, $reservation->getSeat()); - $reservation = $repo->findOneByName('Bratislava Tomorrow '.$i); - $this->assertNotNull($reservation); - $this->assertEquals($i, $reservation->getSeat()); + $reservation = $repo->findOneBy(['name' => 'Bratislava Tomorrow '.$i]); + static::assertNotNull($reservation); + static::assertSame($i, $reservation->getSeat()); - $reservation = $repo->findOneByName('Prague Today '.$i); - $this->assertNotNull($reservation); - $this->assertEquals($i, $reservation->getSeat()); + $reservation = $repo->findOneBy(['name' => 'Prague Today '.$i]); + static::assertNotNull($reservation); + static::assertSame($i, $reservation->getSeat()); } // Change date of the travel - $reservation = $repo->findOneByName('Bratislava Today 1'); + $reservation = $repo->findOneBy(['name' => 'Bratislava Today 1']); $reservation->setTravelDate($tomorrow); $this->em->persist($reservation); $this->em->flush(); // Scan all bus lines // Bratislava Today should have 2 seats - $bratislavaToday = $repo->findBy(array( + $bratislavaToday = $repo->findBy([ 'destination' => 'Bratislava', 'travelDate' => $today, - ), array( 'seat' => 'asc' )); - $this->assertCount(self::SEATS - 1, $bratislavaToday); + ], ['seat' => 'asc']); + static::assertCount(self::SEATS - 1, $bratislavaToday); // Test seat numbers // Should be [ 0, 1 ] - $seats = array_map(function ($r) { return $r->getSeat(); }, $bratislavaToday); - $this->assertEquals(range(0, self::SEATS - 2), $seats, 'Should be seats [ 0, 1 ] to Bratislava Today'); + $seats = array_map(static fn ($r) => $r->getSeat(), $bratislavaToday); + static::assertSame(range(0, self::SEATS - 2), $seats, 'Should be seats [ 0, 1 ] to Bratislava Today'); // Bratislava Tomorrow should have 4 seats - $bratislavaTomorrow = $repo->findBy(array( + $bratislavaTomorrow = $repo->findBy([ 'destination' => 'Bratislava', 'travelDate' => $tomorrow, - ), array( 'seat' => 'asc' )); - $this->assertCount(self::SEATS + 1, $bratislavaTomorrow); + ], ['seat' => 'asc']); + static::assertCount(self::SEATS + 1, $bratislavaTomorrow); // Test seat numbers // Should be [ 0, 1, 2, 3 ] - $seats = array_map(function ($r) { return $r->getSeat(); }, $bratislavaTomorrow); - $this->assertEquals(range(0, self::SEATS), $seats, 'Should be seats [ 0, 1, 2, 3 ] to Bratislava Tomorrow'); + $seats = array_map(static fn ($r) => $r->getSeat(), $bratislavaTomorrow); + static::assertSame(range(0, self::SEATS), $seats, 'Should be seats [ 0, 1, 2, 3 ] to Bratislava Tomorrow'); // Prague Today should have 3 seats - $pragueToday = $repo->findBy(array( + $pragueToday = $repo->findBy([ 'destination' => 'Prague', 'travelDate' => $today, - ), array( 'seat' => 'asc' )); - $this->assertCount(self::SEATS, $pragueToday); + ], ['seat' => 'asc']); + static::assertCount(self::SEATS, $pragueToday); // Test seat numbers - $seats = array_map(function ($r) { return $r->getSeat(); }, $pragueToday); - $this->assertEquals(range(0, self::SEATS - 1), $seats, 'Should be seats [ 0, 1, 2 ] to Prague Today'); + $seats = array_map(static fn ($r) => $r->getSeat(), $pragueToday); + static::assertSame(range(0, self::SEATS - 1), $seats, 'Should be seats [ 0, 1, 2 ] to Prague Today'); } /** - * @test * @group failing */ - public function shouldBeAbleToChangeGroupAndPosition() + public function testShouldBeAbleToChangeGroupAndPosition(): void { $this->populate(); - $this->startQueryLog(); - $repo = $this->em->getRepository(self::ITEM); - $repoCategory = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(Item::class); + $repoCategory = $this->em->getRepository(Category::class); - $vehicle = $repoCategory->findOneByName('Vehicle'); + $vehicle = $repoCategory->findOneBy(['name' => 'Vehicle']); - $vehicles = $repo->findBy(array('category' => $vehicle), array('position' => 'asc')); + $vehicles = $repo->findBy(['category' => $vehicle], ['position' => 'asc']); $position = 1; foreach ($vehicles as $item) { - $this->assertEquals($position, $item->getPosition()); - $position++; + static::assertSame($position, $item->getPosition()); + ++$position; } - $this->assertEquals(31, $position); + static::assertSame(31, $position); - $accessory = $repoCategory->findOneByName('Accessory'); + $accessory = $repoCategory->findOneBy(['name' => 'Accessory']); - $accessories = $repo->findBy(array('category' => $accessory), array('position' => 'asc')); + $accessories = $repo->findBy(['category' => $accessory], ['position' => 'asc']); $position = 1; foreach ($accessories as $item) { - $this->assertEquals($position, $item->getPosition()); - $position++; + static::assertSame($position, $item->getPosition()); + ++$position; } - $this->assertEquals(31, $position); + static::assertSame(31, $position); - $item = $repo->findOneBy(array('category' => $accessory, 'position' => 7)); + $item = $repo->findOneBy(['category' => $accessory, 'position' => 7]); $item->setCategory($vehicle); $item->setPosition(4); $this->em->persist($item); $this->em->flush(); - $this->stopQueryLog(false, true); - unset ($vehicles, $accessories); + unset($vehicles, $accessories); - $vehicles = $repo->findBy(array('category' => $vehicle), array('position' => 'asc')); + $vehicles = $repo->findBy(['category' => $vehicle], ['position' => 'asc']); $position = 1; foreach ($vehicles as $item) { - $this->assertEquals($position, $item->getPosition()); - $position++; + static::assertSame($position, $item->getPosition()); + ++$position; } - $this->assertEquals(32, $position); + static::assertSame(32, $position); - $accessory = $repoCategory->findOneByName('Accessory'); + $accessory = $repoCategory->findOneBy(['name' => 'Accessory']); - $accessories = $repo->findBy(array('category' => $accessory), array('position' => 'asc')); + $accessories = $repo->findBy(['category' => $accessory], ['position' => 'asc']); $position = 1; foreach ($accessories as $item) { - $this->assertEquals($position, $item->getPosition()); - $position++; + static::assertSame($position, $item->getPosition()); + ++$position; + } + static::assertSame(30, $position); + } + + public function testChangePositionWithDateColumn(): void + { + for ($i = 0; $i < 6; ++$i) { + $object = new ItemWithDateColumn(); + $today = new \DateTime('2022-05-22'); + $object->setDate($today); + $object->setPosition($i); + $this->em->persist($object); } - $this->assertEquals(30, $position); + $this->em->flush(); + + $repo = $this->em->getRepository(ItemWithDateColumn::class); + + /** @var ItemWithDateColumn $testItem */ + $testItem = $repo->findOneBy(['id' => 5]); + $testItem->setPosition(1); + + $this->em->persist($testItem); + $this->em->flush(); + + /** @var ItemWithDateColumn $freshItem */ + $freshItem = $repo->findOneBy(['id' => 5]); + /** @var ItemWithDateColumn $freshPreviousItem */ + $freshPreviousItem = $repo->findOneBy(['id' => 2]); + static::assertSame(1, $freshItem->getPosition()); + static::assertSame(2, $freshPreviousItem->getPosition()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::VEHICLE, - self::CAR, - self::ENGINE, - self::BUS, - self::RESERVATION, - self::ITEM, - self::CATEGORY, - ); + return [ + Vehicle::class, + Car::class, + Engine::class, + Bus::class, + Reservation::class, + Item::class, + Category::class, + ItemWithDateColumn::class, + ]; } - private function populate() + private function populate(): void { // engines $v8 = new Engine(); @@ -300,7 +315,7 @@ private function populate() $today = \DateTime::createFromFormat(self::TRAVEL_DATE_FORMAT, self::TODAY); $tomorrow = \DateTime::createFromFormat(self::TRAVEL_DATE_FORMAT, self::TOMORROW); - for ($i = 0; $i < self::SEATS; $i++) { + for ($i = 0; $i < self::SEATS; ++$i) { $reservationBratislava = new Reservation(); $reservationBratislava->setBus($icarus); $reservationBratislava->setDestination('Bratislava'); @@ -327,14 +342,13 @@ private function populate() $categoryVehicle->setName('Vehicle'); $this->em->persist($categoryVehicle); - $categoryAccessory = new Category; + $categoryAccessory = new Category(); $categoryAccessory->setName('Accessory'); $this->em->persist($categoryAccessory); - for ($i = 1; $i <= 60; $i++) - { + for ($i = 1; $i <= 60; ++$i) { $item = new Item(); - $item->setName('Item ' . $i); + $item->setName('Item '.$i); if ($i <= 30) { $item->setCategory($categoryVehicle); $item->setPosition($i); diff --git a/tests/Gedmo/Sortable/SortableTest.php b/tests/Gedmo/Sortable/SortableTest.php index 85df824b91..eada6f2304 100644 --- a/tests/Gedmo/Sortable/SortableTest.php +++ b/tests/Gedmo/Sortable/SortableTest.php @@ -1,111 +1,96 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Sortable; use Doctrine\Common\EventManager; use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; -use Tool\BaseTestCaseORM; -use Sortable\Fixture\Node; -use Sortable\Fixture\Item; -use Sortable\Fixture\Category; -use Sortable\Fixture\SimpleListItem; -use Sortable\Fixture\Author; -use Sortable\Fixture\Paper; -use Sortable\Fixture\Event; -use Sortable\Fixture\Customer; -use Sortable\Fixture\CustomerType; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Gedmo\Sortable\SortableListener; +use Gedmo\Tests\Sortable\Fixture\Author; +use Gedmo\Tests\Sortable\Fixture\Category; +use Gedmo\Tests\Sortable\Fixture\Customer; +use Gedmo\Tests\Sortable\Fixture\CustomerType; +use Gedmo\Tests\Sortable\Fixture\Event; +use Gedmo\Tests\Sortable\Fixture\Item; +use Gedmo\Tests\Sortable\Fixture\Node; +use Gedmo\Tests\Sortable\Fixture\NotifyNode; +use Gedmo\Tests\Sortable\Fixture\Paper; +use Gedmo\Tests\Sortable\Fixture\SimpleListItem; +use Gedmo\Tests\Tool\BaseTestCaseORM; /** * These are tests for sortable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class SortableTest extends BaseTestCaseORM +final class SortableTest extends BaseTestCaseORM { - const NODE = 'Sortable\\Fixture\\Node'; - const ITEM = 'Sortable\\Fixture\\Item'; - const CATEGORY = 'Sortable\\Fixture\\Category'; - const SIMPLE_LIST_ITEM = 'Sortable\\Fixture\\SimpleListItem'; - const AUTHOR = 'Sortable\\Fixture\\Author'; - const PAPER = 'Sortable\\Fixture\\Paper'; - const EVENT = 'Sortable\\Fixture\\Event'; - const CUSTOMER = 'Sortable\\Fixture\\Customer'; - const CUSTOMER_TYPE = 'Sortable\\Fixture\\CustomerType'; - - private $nodeId; - - protected function setUp() + private ?int $nodeId = null; + + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new SortableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - protected function tearDown() + public function testShouldSetSortPositionToInsertedNode(): void { - //$this->stopQueryLog(); + $node = $this->em->find(Node::class, $this->nodeId); + static::assertSame(0, $node->getPosition()); } - /** - * @test - */ - public function shouldSetSortPositionToInsertedNode() - { - $node = $this->em->find(self::NODE, $this->nodeId); - $this->assertEquals(0, $node->getPosition()); - } - - /** - * @test - */ - public function testMoveLastPosition() + public function testMoveLastPosition(): void { - for ($i = 2; $i <= 10; $i++) { + for ($i = 2; $i <= 10; ++$i) { $node = new Node(); - $node->setName("Node".$i); - $node->setPath("/"); + $node->setName('Node'.$i); + $node->setPath('/'); $this->em->persist($node); } - $this->em->flush(); + $this->em->flush(); - $repo = $this->em->getRepository(self::NODE); + $repo = $this->em->getRepository(Node::class); - $node = $repo->findOneByPosition(0); + $node = $repo->findOneBy(['position' => 0]); $node->setPosition(-1); $this->em->flush(); - for ($i = 0; $i <= 8; $i++) { - $node = $repo->findOneByPosition($i); - $this->assertNotNull($node); - $this->assertEquals('Node'.($i+2), $node->getName()); + for ($i = 0; $i <= 8; ++$i) { + $node = $repo->findOneBy(['position' => $i]); + static::assertNotNull($node); + static::assertSame('Node'.($i + 2), $node->getName()); } - $node = $repo->findOneByPosition(9); - $this->assertNotNull($node); - $this->assertEquals('Node1', $node->getName()); - + $node = $repo->findOneBy(['position' => 9]); + static::assertNotNull($node); + static::assertSame('Node1', $node->getName()); } - /** - * @test - */ - public function shouldSortManyNewNodes() + public function testShouldSortManyNewNodes(): void { - for ($i = 2; $i <= 10; $i++) { + for ($i = 2; $i <= 10; ++$i) { $node = new Node(); - $node->setName("Node".$i); - $node->setPath("/"); + $node->setName('Node'.$i); + $node->setPath('/'); $this->em->persist($node); } $this->em->flush(); - $dql = 'SELECT node FROM '.self::NODE.' node'; + $dql = 'SELECT node FROM '.Node::class.' node'; $dql .= ' WHERE node.path = :path ORDER BY node.position'; $nodes = $this->em ->createQuery($dql) @@ -113,161 +98,315 @@ public function shouldSortManyNewNodes() ->getResult() ; - $this->assertCount(10, $nodes); - $this->assertEquals('Node1', $nodes[0]->getName()); - $this->assertEquals(2, $nodes[2]->getPosition()); + static::assertCount(10, $nodes); + static::assertSame('Node1', $nodes[0]->getName()); + static::assertSame(2, $nodes[2]->getPosition()); } - /** - * @test - */ - public function shouldShiftPositionForward() + public function testShouldShiftPositionForward(): void { $node2 = new Node(); - $node2->setName("Node2"); - $node2->setPath("/"); + $node2->setName('Node2'); + $node2->setPath('/'); $this->em->persist($node2); $node = new Node(); - $node->setName("Node3"); - $node->setPath("/"); + $node->setName('Node3'); + $node->setPath('/'); $this->em->persist($node); $node = new Node(); - $node->setName("Node4"); - $node->setPath("/"); + $node->setName('Node4'); + $node->setPath('/'); $this->em->persist($node); $node = new Node(); - $node->setName("Node5"); - $node->setPath("/"); + $node->setName('Node5'); + $node->setPath('/'); $this->em->persist($node); $this->em->flush(); - $this->assertEquals(1, $node2->getPosition()); + static::assertSame(1, $node2->getPosition()); $node2->setPosition(3); $this->em->persist($node2); $this->em->flush(); - $repo = $this->em->getRepository(self::NODE); - $nodes = $repo->getBySortableGroups(array('path' => '/')); + $repo = $this->em->getRepository(Node::class); + $nodes = $repo->getBySortableGroups(['path' => '/']); - $this->assertEquals('Node1', $nodes[0]->getName()); - $this->assertEquals('Node3', $nodes[1]->getName()); - $this->assertEquals('Node4', $nodes[2]->getName()); - $this->assertEquals('Node2', $nodes[3]->getName()); - $this->assertEquals('Node5', $nodes[4]->getName()); + static::assertSame('Node1', $nodes[0]->getName()); + static::assertSame('Node3', $nodes[1]->getName()); + static::assertSame('Node4', $nodes[2]->getName()); + static::assertSame('Node2', $nodes[3]->getName()); + static::assertSame('Node5', $nodes[4]->getName()); - for ($i = 0; $i < count($nodes); $i++) { - $this->assertSame($i, $nodes[$i]->getPosition()); + for ($i = 0; $i < count($nodes); ++$i) { + static::assertSame($i, $nodes[$i]->getPosition()); } } - /** - * @test - */ - public function shouldShiftPositionBackward() + public function testShouldShiftPositionsProperlyWhenMoreThanOneWasUpdated(): void + { + $node2 = new Node(); + $node2->setName('Node2'); + $node2->setPath('/'); + $this->em->persist($node2); + + $node3 = new Node(); + $node3->setName('Node3'); + $node3->setPath('/'); + $this->em->persist($node3); + + $node = new Node(); + $node->setName('Node4'); + $node->setPath('/'); + $this->em->persist($node); + + $node = new Node(); + $node->setName('Node5'); + $node->setPath('/'); + $this->em->persist($node); + + $this->em->flush(); + + static::assertSame(1, $node2->getPosition()); + $node2->setPosition(3); + $node3->setPosition(4); + $this->em->persist($node2); + $this->em->persist($node3); + $this->em->flush(); + + $repo = $this->em->getRepository(Node::class); + $nodes = $repo->getBySortableGroups(['path' => '/']); + + static::assertSame('Node1', $nodes[0]->getName()); + static::assertSame('Node4', $nodes[1]->getName()); + static::assertSame('Node5', $nodes[2]->getName()); + static::assertSame('Node2', $nodes[3]->getName()); + static::assertSame('Node3', $nodes[4]->getName()); + + for ($i = 0; $i < count($nodes); ++$i) { + static::assertSame($i, $nodes[$i]->getPosition()); + } + } + + public function testShouldShiftPositionBackward(): void { $node = new Node(); - $node->setName("Node2"); - $node->setPath("/"); + $node->setName('Node2'); + $node->setPath('/'); $this->em->persist($node); $node = new Node(); - $node->setName("Node3"); - $node->setPath("/"); + $node->setName('Node3'); + $node->setPath('/'); $this->em->persist($node); $node2 = new Node(); - $node2->setName("Node4"); - $node2->setPath("/"); + $node2->setName('Node4'); + $node2->setPath('/'); $this->em->persist($node2); $node = new Node(); - $node->setName("Node5"); - $node->setPath("/"); + $node->setName('Node5'); + $node->setPath('/'); $this->em->persist($node); $this->em->flush(); - $this->assertEquals(3, $node2->getPosition()); + static::assertSame(3, $node2->getPosition()); $node2->setPosition(1); $this->em->persist($node2); $this->em->flush(); $this->em->clear(); // to reload from database - $repo = $this->em->getRepository(self::NODE); - $nodes = $repo->getBySortableGroups(array('path' => '/')); + $repo = $this->em->getRepository(Node::class); + $nodes = $repo->getBySortableGroups(['path' => '/']); - $this->assertEquals('Node1', $nodes[0]->getName()); - $this->assertEquals('Node4', $nodes[1]->getName()); - $this->assertEquals('Node2', $nodes[2]->getName()); - $this->assertEquals('Node3', $nodes[3]->getName()); - $this->assertEquals('Node5', $nodes[4]->getName()); + static::assertSame('Node1', $nodes[0]->getName()); + static::assertSame('Node4', $nodes[1]->getName()); + static::assertSame('Node2', $nodes[2]->getName()); + static::assertSame('Node3', $nodes[3]->getName()); + static::assertSame('Node5', $nodes[4]->getName()); - for ($i = 0; $i < count($nodes); $i++) { - $this->assertSame($i, $nodes[$i]->getPosition()); + for ($i = 0; $i < count($nodes); ++$i) { + static::assertSame($i, $nodes[$i]->getPosition()); } } + public function testShouldSyncPositionAfterDelete(): void + { + $repo = $this->em->getRepository(Node::class); + + $node2 = new Node(); + $node2->setName('Node2'); + $node2->setPath('/'); + $this->em->persist($node2); + + $node3 = new Node(); + $node3->setName('Node3'); + $node3->setPath('/'); + $this->em->persist($node3); + + $this->em->flush(); + + $node1 = $repo->findOneBy(['name' => 'Node1']); + $this->em->remove($node2); + $this->em->flush(); + + // test if synced on objects in memory correctly + static::assertSame(0, $node1->getPosition()); + static::assertSame(1, $node3->getPosition()); + + // test if persisted correctly + $this->em->clear(); + $nodes = $repo->findAll(); + static::assertCount(2, $nodes); + static::assertSame(0, $nodes[0]->getPosition()); + static::assertSame(1, $nodes[1]->getPosition()); + } + /** - * @test + * Test if the sorting is correct if multiple items are deleted. + * + * Example: + * Position | Element | Action | Expected Position + * 0 | Node1 | | 0 + * 1 | Node2 | delete | + * 2 | Node3 | delete | + * 3 | Node4 | | 1 */ - public function shouldSyncPositionAfterDelete() + public function testShouldSyncPositionAfterMultipleDeletes(): void { - $repo = $this->em->getRepository(self::NODE); + $repo = $this->em->getRepository(Node::class); $node2 = new Node(); - $node2->setName("Node2"); - $node2->setPath("/"); + $node2->setName('Node2'); + $node2->setPath('/'); $this->em->persist($node2); $node3 = new Node(); - $node3->setName("Node3"); - $node3->setPath("/"); + $node3->setName('Node3'); + $node3->setPath('/'); $this->em->persist($node3); + $node4 = new Node(); + $node4->setName('Node4'); + $node4->setPath('/'); + $this->em->persist($node4); + $this->em->flush(); - $node1 = $repo->findOneByName('Node1'); + $node1 = $repo->findOneBy(['name' => 'Node1']); $this->em->remove($node2); + $this->em->remove($node3); $this->em->flush(); // test if synced on objects in memory correctly - $this->assertEquals(0, $node1->getPosition()); - $this->assertEquals(1, $node3->getPosition()); + static::assertSame(0, $node1->getPosition()); + static::assertSame(1, $node4->getPosition()); // test if persisted correctly $this->em->clear(); $nodes = $repo->findAll(); - $this->assertCount(2, $nodes); - $this->assertEquals(0, $nodes[0]->getPosition()); - $this->assertEquals(1, $nodes[1]->getPosition()); + static::assertCount(2, $nodes); + static::assertSame(0, $nodes[0]->getPosition()); + static::assertSame(1, $nodes[1]->getPosition()); + } + + /** + * Test if the sorting is correct if multiple items are added and deleted. + * + * Example: + * Position | Element | Action | Expected Position + * 0 | Node1 | | 0 + * 1 | Node2 | delete | + * 2 | Node3 | delete | + * 3 | Node4 | | 1 + * | Node5 | add | 2 + * | Node6 | add | 3 + */ + public function testShouldSyncPositionAfterMultipleAddsAndMultipleDeletes(): void + { + $repo = $this->em->getRepository(Node::class); + + $node2 = new Node(); + $node2->setName('Node2'); + $node2->setPath('/'); + $this->em->persist($node2); + + $node3 = new Node(); + $node3->setName('Node3'); + $node3->setPath('/'); + $this->em->persist($node3); + + $node4 = new Node(); + $node4->setName('Node4'); + $node4->setPath('/'); + $this->em->persist($node4); + + $this->em->flush(); + + $node1 = $repo->findOneBy(['name' => 'Node1']); + + $this->em->remove($node2); + + $node5 = new Node(); + $node5->setName('Node5'); + $node5->setPath('/'); + $this->em->persist($node5); + + $node6 = new Node(); + $node6->setName('Node6'); + $node6->setPath('/'); + $this->em->persist($node6); + + $this->em->remove($node3); + + $this->em->flush(); + + // test if synced on objects in memory correctly + static::assertSame(0, $node1->getPosition()); + static::assertSame(1, $node4->getPosition()); + static::assertSame(2, $node5->getPosition()); + static::assertSame(3, $node6->getPosition()); + + // test if persisted correctly + $this->em->clear(); + $nodes = $repo->findAll(); + static::assertCount(4, $nodes); + static::assertSame(0, $nodes[0]->getPosition()); + static::assertSame('Node1', $nodes[0]->getName()); + static::assertSame(1, $nodes[1]->getPosition()); + static::assertSame('Node4', $nodes[1]->getName()); + static::assertSame(2, $nodes[2]->getPosition()); + static::assertSame('Node5', $nodes[2]->getName()); + static::assertSame(3, $nodes[3]->getPosition()); + static::assertSame('Node6', $nodes[3]->getName()); } /** * This is a test case for issue #1209 - * @test */ - public function shouldRollbackPositionAfterExceptionOnDelete() + public function testShouldRollbackPositionAfterExceptionOnDelete(): void { - $repo = $this->em->getRepository(self::CUSTOMER_TYPE); + $repo = $this->em->getRepository(CustomerType::class); $customerType1 = new CustomerType(); - $customerType1->setName("CustomerType1"); + $customerType1->setName('CustomerType1'); $this->em->persist($customerType1); $customerType2 = new CustomerType(); - $customerType2->setName("CustomerType2"); + $customerType2->setName('CustomerType2'); $this->em->persist($customerType2); $customerType3 = new CustomerType(); - $customerType3->setName("CustomerType3"); + $customerType3->setName('CustomerType3'); $this->em->persist($customerType3); $customer = new Customer(); - $customer->setName("Customer"); + $customer->setName('Customer'); $customer->setType($customerType2); $this->em->persist($customer); @@ -279,246 +418,175 @@ public function shouldRollbackPositionAfterExceptionOnDelete() $this->em->remove($customerType2); $this->em->flush(); - $this->fail('Foreign key constraint violation exception not thrown.'); + static::fail('Foreign key constraint violation exception not thrown.'); } catch (ForeignKeyConstraintViolationException $e) { $customerTypes = $repo->findAll(); - $this->assertCount(3, $customerTypes); + static::assertCount(3, $customerTypes); - $this->assertEquals(0, $customerTypes[0]->getPosition(), 'The sorting position has not been rolled back.'); - $this->assertEquals(1, $customerTypes[1]->getPosition(), 'The sorting position has not been rolled back.'); - $this->assertEquals(2, $customerTypes[2]->getPosition(), 'The sorting position has not been rolled back.'); + static::assertSame(0, $customerTypes[0]->getPosition(), 'The sorting position has not been rolled back.'); + static::assertSame(1, $customerTypes[1]->getPosition(), 'The sorting position has not been rolled back.'); + static::assertSame(2, $customerTypes[2]->getPosition(), 'The sorting position has not been rolled back.'); } } - /** - * @test - */ - public function shouldGroupByAssociation() + public function testShouldGroupByAssociation(): void { $category1 = new Category(); - $category1->setName("Category1"); + $category1->setName('Category1'); $this->em->persist($category1); $category2 = new Category(); - $category2->setName("Category2"); + $category2->setName('Category2'); $this->em->persist($category2); $this->em->flush(); $item3 = new Item(); - $item3->setName("Item3"); + $item3->setName('Item3'); $item3->setCategory($category1); $this->em->persist($item3); $item4 = new Item(); - $item4->setName("Item4"); + $item4->setName('Item4'); $item4->setCategory($category1); $this->em->persist($item4); $this->em->flush(); $item1 = new Item(); - $item1->setName("Item1"); + $item1->setName('Item1'); $item1->setPosition(0); $item1->setCategory($category1); $this->em->persist($item1); $item2 = new Item(); - $item2->setName("Item2"); + $item2->setName('Item2'); $item2->setPosition(0); $item2->setCategory($category1); $this->em->persist($item2); $item2 = new Item(); - $item2->setName("Item2_2"); + $item2->setName('Item2_2'); $item2->setPosition(0); $item2->setCategory($category2); $this->em->persist($item2); $this->em->flush(); $item1 = new Item(); - $item1->setName("Item1_2"); + $item1->setName('Item1_2'); $item1->setPosition(0); $item1->setCategory($category2); $this->em->persist($item1); $this->em->flush(); - $repo = $this->em->getRepository(self::CATEGORY); - $category1 = $repo->findOneByName('Category1'); - $category2 = $repo->findOneByName('Category2'); + $repo = $this->em->getRepository(Category::class); + $category1 = $repo->findOneBy(['name' => 'Category1']); + $category2 = $repo->findOneBy(['name' => 'Category2']); - $repo = $this->em->getRepository(self::ITEM); + $repo = $this->em->getRepository(Item::class); - $items = $repo->getBySortableGroups(array('category' => $category1)); + $items = $repo->getBySortableGroups(['category' => $category1]); - $this->assertEquals("Item1", $items[0]->getName()); - $this->assertEquals("Category1", $items[0]->getCategory()->getName()); + static::assertSame('Item1', $items[0]->getName()); + static::assertSame('Category1', $items[0]->getCategory()->getName()); - $this->assertEquals("Item2", $items[1]->getName()); - $this->assertEquals("Category1", $items[1]->getCategory()->getName()); + static::assertSame('Item2', $items[1]->getName()); + static::assertSame('Category1', $items[1]->getCategory()->getName()); - $this->assertEquals("Item3", $items[2]->getName()); - $this->assertEquals("Category1", $items[2]->getCategory()->getName()); + static::assertSame('Item3', $items[2]->getName()); + static::assertSame('Category1', $items[2]->getCategory()->getName()); - $this->assertEquals("Item4", $items[3]->getName()); - $this->assertEquals("Category1", $items[3]->getCategory()->getName()); + static::assertSame('Item4', $items[3]->getName()); + static::assertSame('Category1', $items[3]->getCategory()->getName()); - $items = $repo->getBySortableGroups(array('category' => $category2)); + $items = $repo->getBySortableGroups(['category' => $category2]); - $this->assertEquals("Item1_2", $items[0]->getName()); - $this->assertEquals("Category2", $items[0]->getCategory()->getName()); + static::assertSame('Item1_2', $items[0]->getName()); + static::assertSame('Category2', $items[0]->getCategory()->getName()); - $this->assertEquals("Item2_2", $items[1]->getName()); - $this->assertEquals("Category2", $items[1]->getCategory()->getName()); + static::assertSame('Item2_2', $items[1]->getName()); + static::assertSame('Category2', $items[1]->getCategory()->getName()); } - /** - * @test - */ - public function shouldGroupByNewAssociation() + public function testShouldGroupByNewAssociation(): void { $category1 = new Category(); - $category1->setName("Category1"); + $category1->setName('Category1'); $item1 = new Item(); - $item1->setName("Item1"); + $item1->setName('Item1'); $item1->setPosition(0); $item1->setCategory($category1); $this->em->persist($item1); $this->em->persist($category1); $this->em->flush(); - $repo = $this->em->getRepository(self::CATEGORY); - $category1 = $repo->findOneByName('Category1'); - - $repo = $this->em->getRepository(self::ITEM); - - $items = $repo->getBySortableGroups(array('category' => $category1)); - - $this->assertEquals("Item1", $items[0]->getName()); - $this->assertEquals("Category1", $items[0]->getCategory()->getName()); - } - - /** - * @test - */ - public function shouldInsertInbetween() - { - $this->markTestIncomplete('Currently it is not supported to change the position of a record and insert a new one in front of it in one step.'); - - $item1 = new Item(); - $item1->setName("Item1"); - $this->em->persist($item1); - - $item3 = new Item(); - $item3->setName("Item3"); - $this->em->persist($item3); - - $this->em->flush(); - - // update $item3's position - $item3->setPosition(2); + $repo = $this->em->getRepository(Category::class); + $category1 = $repo->findOneBy(['name' => 'Category1']); - // and insert a further item between $item1 and $item3 - $item2 = new Item(); - $item2->setName("Item2"); - $item2->setPosition(1); - $this->em->persist($item2); - - $this->em->flush(); + $repo = $this->em->getRepository(Item::class); - $repo = $this->em->getRepository(self::ITEM); - $items = $repo->findBy(array(), array('position' => 'asc')); + $items = $repo->getBySortableGroups(['category' => $category1]); - $this->assertEquals("Item1", $items[0]->getName()); - $this->assertEquals(0, $items[0]->getPosition()); - $this->assertEquals("Item2", $items[1]->getName()); - $this->assertEquals(1, $items[1]->getPosition()); - $this->assertEquals("Item3", $items[2]->getName()); - $this->assertEquals(2, $items[2]->getPosition()); + static::assertSame('Item1', $items[0]->getName()); + static::assertSame('Category1', $items[0]->getCategory()->getName()); } - /** - * @test - */ - public function shouldGroupByDateTimeValue() + public function testShouldGroupByDateTimeValue(): void { $event1 = new Event(); - $event1->setDateTime(new \DateTime("2012-09-15 00:00:00")); - $event1->setName("Event1"); + $event1->setDateTime(new \DateTime('2012-09-15 00:00:00')); + $event1->setName('Event1'); $this->em->persist($event1); $event2 = new Event(); - $event2->setDateTime(new \DateTime("2012-09-15 00:00:00")); - $event2->setName("Event2"); + $event2->setDateTime(new \DateTime('2012-09-15 00:00:00')); + $event2->setName('Event2'); $this->em->persist($event2); $event3 = new Event(); - $event3->setDateTime(new \DateTime("2012-09-16 00:00:00")); - $event3->setName("Event3"); + $event3->setDateTime(new \DateTime('2012-09-16 00:00:00')); + $event3->setName('Event3'); $this->em->persist($event3); $this->em->flush(); $event4 = new Event(); - $event4->setDateTime(new \DateTime("2012-09-15 00:00:00")); - $event4->setName("Event4"); + $event4->setDateTime(new \DateTime('2012-09-15 00:00:00')); + $event4->setName('Event4'); $this->em->persist($event4); $event5 = new Event(); - $event5->setDateTime(new \DateTime("2012-09-16 00:00:00")); - $event5->setName("Event5"); + $event5->setDateTime(new \DateTime('2012-09-16 00:00:00')); + $event5->setName('Event5'); $this->em->persist($event5); $this->em->flush(); - $this->assertEquals(0, $event1->getPosition()); - $this->assertEquals(1, $event2->getPosition()); - $this->assertEquals(0, $event3->getPosition()); - $this->assertEquals(2, $event4->getPosition()); - $this->assertEquals(1, $event5->getPosition()); - } - - /** - * @test - */ - public function shouldFixIssue219() - { - $item1 = new SimpleListItem(); - $item1->setName("Item 1"); - $this->em->persist($item1); - - $this->em->flush(); - - $item1->setName("Update..."); - $item1->setPosition(1); - $this->em->persist($item1); - $this->em->flush(); - - $this->em->remove($item1); - $this->em->flush(); + static::assertSame(0, $event1->getPosition()); + static::assertSame(1, $event2->getPosition()); + static::assertSame(0, $event3->getPosition()); + static::assertSame(2, $event4->getPosition()); + static::assertSame(1, $event5->getPosition()); } - /** - * @test - */ - public function shouldFixIssue226() + public function testShouldFixIssue226(): void { $paper1 = new Paper(); - $paper1->setName("Paper1"); + $paper1->setName('Paper1'); $this->em->persist($paper1); $paper2 = new Paper(); - $paper2->setName("Paper2"); + $paper2->setName('Paper2'); $this->em->persist($paper2); $author1 = new Author(); - $author1->setName("Author1"); + $author1->setName('Author1'); $author1->setPaper($paper1); $author2 = new Author(); - $author2->setName("Author2"); + $author2->setName('Author2'); $author2->setPaper($paper1); $author3 = new Author(); - $author3->setName("Author3"); + $author3->setName('Author3'); $author3->setPaper($paper2); $this->em->persist($author1); @@ -526,19 +594,19 @@ public function shouldFixIssue226() $this->em->persist($author3); $this->em->flush(); - $this->assertEquals(0, $author1->getPosition()); - $this->assertEquals(1, $author2->getPosition()); - $this->assertEquals(0, $author3->getPosition()); + static::assertSame(0, $author1->getPosition()); + static::assertSame(1, $author2->getPosition()); + static::assertSame(0, $author3->getPosition()); - //update position + // update position $author3->setPaper($paper1); $author3->setPosition(0); // same as before, no changes $this->em->persist($author3); $this->em->flush(); - $this->assertEquals(1, $author1->getPosition()); - $this->assertEquals(2, $author2->getPosition()); - $this->assertEquals(0, $author3->getPosition()); + static::assertSame(1, $author1->getPosition()); + static::assertSame(2, $author2->getPosition()); + static::assertSame(0, $author3->getPosition()); // this is failing for whatever reasons $author3->setPosition(0); @@ -547,119 +615,87 @@ public function shouldFixIssue226() $this->em->clear(); // @TODO: this should not be required - $author1 = $this->em->find(self::AUTHOR, $author1->getId()); - $author2 = $this->em->find(self::AUTHOR, $author2->getId()); - $author3 = $this->em->find(self::AUTHOR, $author3->getId()); - - $this->assertEquals(1, $author1->getPosition()); - $this->assertEquals(2, $author2->getPosition()); - $this->assertEquals(0, $author3->getPosition()); - } + $author1 = $this->em->find(Author::class, $author1->getId()); + $author2 = $this->em->find(Author::class, $author2->getId()); + $author3 = $this->em->find(Author::class, $author3->getId()); - /** - * @test - */ - public function shouldFixIssue275() - { - $nodes = array(); - for ($i = 2; $i <= 10; $i++) { - $node = new Node(); - $node->setName("Node".$i); - $node->setPath("/"); - $this->em->persist($node); - $nodes[] = $node; - } - $this->em->flush(); - - $node1 = $this->em->find(self::NODE, $this->nodeId); - $this->em->remove($node1); - $this->em->flush(); - - for ($i = 1; $i <= 9; $i++) { - $nodes[$i-1]->setPosition($i); - $this->em->persist($nodes[$i-1]); - } - $this->em->flush(); + static::assertSame(1, $author1->getPosition()); + static::assertSame(2, $author2->getPosition()); + static::assertSame(0, $author3->getPosition()); } - /** - * @test - */ - public function shouldFixIssue1445() + public function testShouldFixIssue1445(): void { $paper1 = new Paper(); - $paper1->setName("Paper1"); + $paper1->setName('Paper1'); $this->em->persist($paper1); $paper2 = new Paper(); - $paper2->setName("Paper2"); + $paper2->setName('Paper2'); $this->em->persist($paper2); $author1 = new Author(); - $author1->setName("Author1"); + $author1->setName('Author1'); $author1->setPaper($paper1); $author2 = new Author(); - $author2->setName("Author2"); + $author2->setName('Author2'); $author2->setPaper($paper1); $this->em->persist($author1); $this->em->persist($author2); $this->em->flush(); - $this->assertEquals(0, $author1->getPosition()); - $this->assertEquals(1, $author2->getPosition()); + static::assertSame(0, $author1->getPosition()); + static::assertSame(1, $author2->getPosition()); - //update position + // update position $author2->setPaper($paper2); $author2->setPosition(0); // Position has changed author2 was at position 1 in paper1 and now 0 in paper2, so it can be in changeSets $this->em->persist($author2); $this->em->flush(); - $this->assertEquals(0, $author1->getPosition()); - $this->assertEquals(0, $author2->getPosition()); + static::assertSame(0, $author1->getPosition()); + static::assertSame(0, $author2->getPosition()); $this->em->clear(); // @TODO: this should not be required - $repo = $this->em->getRepository(self::AUTHOR); + $repo = $this->em->getRepository(Author::class); $author1 = $repo->findOneBy(['id' => $author1->getId()]); $author2 = $repo->findOneBy(['id' => $author2->getId()]); - $this->assertEquals(0, $author1->getPosition()); - $this->assertEquals(0, $author2->getPosition()); + static::assertSame(0, $author1->getPosition()); + static::assertSame(0, $author2->getPosition()); } - /** - * @test - */ - public function shouldFixIssue1462() + public function testShouldFixIssue1462(): void { $paper1 = new Paper(); - $paper1->setName("Paper1"); + $paper1->setName('Paper1'); $this->em->persist($paper1); $paper2 = new Paper(); - $paper2->setName("Paper2"); + $paper2->setName('Paper2'); $this->em->persist($paper2); $author1 = new Author(); - $author1->setName("Author1"); + $author1->setName('Author1'); $author1->setPaper($paper1); $author2 = new Author(); - $author2->setName("Author2"); + $author2->setName('Author2'); $author2->setPaper($paper1); $author3 = new Author(); - $author3->setName("Author3"); + $author3->setName('Author3'); $author3->setPaper($paper2); $author4 = new Author(); - $author4->setName("Author4"); + $author4->setName('Author4'); $author4->setPaper($paper2); $author5 = new Author(); - $author5->setName("Author5"); + $author5->setName('Author5'); $author5->setPaper($paper1); $this->em->persist($author1); @@ -669,148 +705,167 @@ public function shouldFixIssue1462() $this->em->persist($author5); $this->em->flush(); - $this->assertEquals(0, $author1->getPosition()); - $this->assertEquals(1, $author2->getPosition()); - $this->assertEquals(2, $author5->getPosition()); + static::assertSame(0, $author1->getPosition()); + static::assertSame(1, $author2->getPosition()); + static::assertSame(2, $author5->getPosition()); - $this->assertEquals(0, $author3->getPosition()); - $this->assertEquals(1, $author4->getPosition()); + static::assertSame(0, $author3->getPosition()); + static::assertSame(1, $author4->getPosition()); // update paper: the position is still 1. $author4->setPaper($paper1); $this->em->persist($author4); $this->em->flush(); - $this->assertEquals(0, $author1->getPosition()); - $this->assertEquals(1, $author4->getPosition()); - $this->assertEquals(2, $author2->getPosition()); - $this->assertEquals(3, $author5->getPosition()); + static::assertSame(0, $author1->getPosition()); + static::assertSame(1, $author4->getPosition()); + static::assertSame(2, $author2->getPosition()); + static::assertSame(3, $author5->getPosition()); - $this->assertEquals(0, $author3->getPosition()); + static::assertSame(0, $author3->getPosition()); $this->em->clear(); // @TODO: this should not be required - $repo = $this->em->getRepository(self::AUTHOR); + $repo = $this->em->getRepository(Author::class); $author1 = $repo->findOneBy(['id' => $author1->getId()]); $author2 = $repo->findOneBy(['id' => $author2->getId()]); $author3 = $repo->findOneBy(['id' => $author3->getId()]); $author4 = $repo->findOneBy(['id' => $author4->getId()]); $author5 = $repo->findOneBy(['id' => $author5->getId()]); - $this->assertEquals(0, $author1->getPosition()); - $this->assertEquals(1, $author4->getPosition()); - $this->assertEquals(2, $author2->getPosition()); - $this->assertEquals(3, $author5->getPosition()); + static::assertSame(0, $author1->getPosition()); + static::assertSame(1, $author4->getPosition()); + static::assertSame(2, $author2->getPosition()); + static::assertSame(3, $author5->getPosition()); - $this->assertEquals(0, $author3->getPosition()); + static::assertSame(0, $author3->getPosition()); } - /** - * @test - */ - public function positionShouldBeTheSameAfterFlush() + public function testPositionShouldBeTheSameAfterFlush(): void { - $nodes = array(); - for ($i = 2; $i <= 10; $i++) { + $nodes = []; + for ($i = 2; $i <= 10; ++$i) { $node = new Node(); - $node->setName("Node".$i); - $node->setPath("/"); + $node->setName('Node'.$i); + $node->setPath('/'); $this->em->persist($node); $nodes[] = $node; } $this->em->flush(); - $node1 = $this->em->find(self::NODE, $this->nodeId); + $node1 = $this->em->find(Node::class, $this->nodeId); $node1->setPosition(5); $this->em->flush(); - $this->assertEquals(5, $node1->getPosition()); + static::assertSame(5, $node1->getPosition()); $this->em->detach($node1); - $node1 = $this->em->find(self::NODE, $this->nodeId); - $this->assertEquals(5, $node1->getPosition()); + $node1 = $this->em->find(Node::class, $this->nodeId); + static::assertSame(5, $node1->getPosition()); } - /** - * @test - */ - public function testIncrementPositionOfLastObjectByOne() + public function testIncrementPositionOfLastObjectByOne(): void { - $node0 = $this->em->find(self::NODE, $this->nodeId); + $node0 = $this->em->find(Node::class, $this->nodeId); - $nodes = array($node0); + $nodes = [$node0]; - for ($i = 2; $i <= 5; $i++) { + for ($i = 2; $i <= 5; ++$i) { $node = new Node(); - $node->setName("Node".$i); - $node->setPath("/"); + $node->setName('Node'.$i); + $node->setPath('/'); $this->em->persist($node); $nodes[] = $node; } $this->em->flush(); - $this->assertEquals(4, $nodes[4]->getPosition()); + static::assertSame(4, $nodes[4]->getPosition()); $node4NewPosition = $nodes[4]->getPosition(); - $node4NewPosition++; + ++$node4NewPosition; $nodes[4]->setPosition($node4NewPosition); $this->em->persist($nodes[4]); $this->em->flush(); - $this->assertEquals(4, $nodes[4]->getPosition()); + static::assertSame(4, $nodes[4]->getPosition()); } - /** - * @test - */ - public function testSetOutOfBoundsHighPosition() + public function testSetOutOfBoundsHighPosition(): void { - $node0 = $this->em->find(self::NODE, $this->nodeId); + $node0 = $this->em->find(Node::class, $this->nodeId); - $nodes = array($node0); + $nodes = [$node0]; - for ($i = 2; $i <= 5; $i++) { + for ($i = 2; $i <= 5; ++$i) { $node = new Node(); - $node->setName("Node".$i); - $node->setPath("/"); + $node->setName('Node'.$i); + $node->setPath('/'); $this->em->persist($node); $nodes[] = $node; } $this->em->flush(); - $this->assertEquals(4, $nodes[4]->getPosition()); + static::assertSame(4, $nodes[4]->getPosition()); $nodes[4]->setPosition(100); $this->em->persist($nodes[4]); $this->em->flush(); - $this->assertEquals(4, $nodes[4]->getPosition()); + static::assertSame(4, $nodes[4]->getPosition()); } - protected function getUsedEntityFixtures() + public function testShouldFixIssue1809(): void { - return array( - self::NODE, - self::ITEM, - self::CATEGORY, - self::SIMPLE_LIST_ITEM, - self::AUTHOR, - self::PAPER, - self::EVENT, - self::CUSTOMER, - self::CUSTOMER_TYPE, - ); + if (!class_exists(AnnotationDriver::class)) { + static::markTestSkipped('Test uses a fixture using the deprecated "NOTIFY" change tracking policy.'); + } + + $manager = $this->em; + $nodes = []; + for ($i = 1; $i <= 3; ++$i) { + $node = new NotifyNode(); + $node->setName('Node'.$i); + $node->setPath('/'); + $manager->persist($node); + $nodes[] = $node; + $manager->flush(); + } + foreach ($nodes as $i => $node) { + $position = $node->getPosition(); + static::assertSame($i, $position); + } + } + + protected function getUsedEntityFixtures(): array + { + $fixtures = [ + Node::class, + Item::class, + Category::class, + SimpleListItem::class, + Author::class, + Paper::class, + Event::class, + Customer::class, + CustomerType::class, + ]; + + if (class_exists(AnnotationDriver::class)) { + $fixtures[] = NotifyNode::class; + } + + return $fixtures; } - private function populate() + private function populate(): void { $node = new Node(); - $node->setName("Node1"); - $node->setPath("/"); + $node->setName('Node1'); + $node->setPath('/'); $this->em->persist($node); $this->em->flush(); diff --git a/tests/Gedmo/TestActorProvider.php b/tests/Gedmo/TestActorProvider.php new file mode 100644 index 0000000000..6bf03c3d73 --- /dev/null +++ b/tests/Gedmo/TestActorProvider.php @@ -0,0 +1,42 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests; + +use Gedmo\Tool\ActorProviderInterface; + +final class TestActorProvider implements ActorProviderInterface +{ + /** + * @var object|string|null + */ + private $actor; + + /** + * @param object|string|null $actor + */ + public function __construct($actor) + { + if (!is_string($actor) && !is_object($actor) && null !== $actor) { + throw new \TypeError(sprintf('The actor must be a string, an object, or null, "%s" given.', gettype($actor))); + } + + $this->actor = $actor; + } + + /** + * @return object|string|null + */ + public function getActor() + { + return $this->actor; + } +} diff --git a/tests/Gedmo/TestIpAddressProvider.php b/tests/Gedmo/TestIpAddressProvider.php new file mode 100644 index 0000000000..bc6283e857 --- /dev/null +++ b/tests/Gedmo/TestIpAddressProvider.php @@ -0,0 +1,29 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests; + +use Gedmo\Tool\IpAddressProviderInterface; + +final class TestIpAddressProvider implements IpAddressProviderInterface +{ + private ?string $address; + + public function __construct(?string $address) + { + $this->address = $address; + } + + public function getAddress(): ?string + { + return $this->address; + } +} diff --git a/tests/Gedmo/Timestampable/AttributeChangeTest.php b/tests/Gedmo/Timestampable/AttributeChangeTest.php new file mode 100644 index 0000000000..25fecb00d8 --- /dev/null +++ b/tests/Gedmo/Timestampable/AttributeChangeTest.php @@ -0,0 +1,118 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Timestampable\Fixture\Attribute\TitledArticle; +use Gedmo\Tests\Tool\BaseTestCaseORM; + +/** + * These are tests for Timestampable behavior + * + * @author Ivan Borzenkov + * + * @see http://www.gediminasm.org + * + * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * + * @requires PHP >= 8.0 + * + * @todo This test requires {@see ChangeTest} to have been run first to load the {@see TimestampableListenerStub} + */ +final class AttributeChangeTest extends BaseTestCaseORM +{ + /** + * @var TimestampableListenerStub + */ + protected $listener; + + protected function setUp(): void + { + parent::setUp(); + + $this->listener = new TimestampableListenerStub(); + $this->listener->eventAdapter = new EventAdapterORMStub(); + + $evm = new EventManager(); + $evm->addEventSubscriber($this->listener); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testChange(): void + { + $test = new TitledArticle(); + $test->setTitle('Test'); + $test->setText('Test'); + $test->setState('Open'); + + $currentDate = new \DateTime(); + $this->listener->eventAdapter->setDateValue($currentDate); + + $this->em->persist($test); + $this->em->flush(); + $this->em->clear(); + + $test = $this->em->getRepository(TitledArticle::class)->findOneBy(['title' => 'Test']); + $test->setTitle('New Title'); + $test->setState('Closed'); + $this->em->persist($test); + $this->em->flush(); + $this->em->clear(); + // Changed. + static::assertSame( + $currentDate->format('Y-m-d H:i:s'), + $test->getChtitle()->format('Y-m-d H:i:s') + ); + static::assertSame( + $currentDate->format('Y-m-d H:i:s'), + $test->getClosed()->format('Y-m-d H:i:s') + ); + + $anotherDate = \DateTime::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00'); + $this->listener->eventAdapter->setDateValue($anotherDate); + + $test = $this->em->getRepository(TitledArticle::class)->findOneBy(['title' => 'New Title']); + $test->setText('New Text'); + $test->setState('Open'); + $this->em->persist($test); + $this->em->flush(); + $this->em->clear(); + // Not Changed. + static::assertSame( + $currentDate->format('Y-m-d H:i:s'), + $test->getChtitle()->format('Y-m-d H:i:s') + ); + static::assertSame( + $currentDate->format('Y-m-d H:i:s'), + $test->getClosed()->format('Y-m-d H:i:s') + ); + + $test = $this->em->getRepository(TitledArticle::class)->findOneBy(['title' => 'New Title']); + $test->setState('Published'); + $this->em->persist($test); + $this->em->flush(); + $this->em->clear(); + // Changed. + static::assertSame( + $anotherDate->format('Y-m-d H:i:s'), + $test->getClosed()->format('Y-m-d H:i:s') + ); + } + + protected function getUsedEntityFixtures(): array + { + return [ + TitledArticle::class, + ]; + } +} diff --git a/tests/Gedmo/Timestampable/CarbonTest.php b/tests/Gedmo/Timestampable/CarbonTest.php new file mode 100644 index 0000000000..4d55c2f7ac --- /dev/null +++ b/tests/Gedmo/Timestampable/CarbonTest.php @@ -0,0 +1,151 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable; + +use Carbon\Carbon; +use Carbon\CarbonImmutable; +use Carbon\Doctrine\DateTimeImmutableType; +use Carbon\Doctrine\DateTimeType; +use DateTime; +use Doctrine\Common\EventManager; +use Doctrine\DBAL\Types\DateType; +use Doctrine\DBAL\Types\Type as DoctrineType; +use Doctrine\DBAL\Types\Types; +use Gedmo\Tests\Timestampable\Fixture\ArticleCarbon; +use Gedmo\Tests\Timestampable\Fixture\Author; +use Gedmo\Tests\Timestampable\Fixture\CommentCarbon; +use Gedmo\Tests\Timestampable\Fixture\Type; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Timestampable\TimestampableListener; + +final class CarbonTest extends BaseTestCaseORM +{ + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $evm->addEventSubscriber(new TimestampableListener()); + + $this->getDefaultMockSqliteEntityManager($evm); + + /** + * DATE_MUTABLE => Carbon + * DATETIME_MUTABLE => CarbonImmutable + * TIME_MUTABLE => DateTime + */ + DoctrineType::overrideType(Types::DATE_MUTABLE, DateTimeType::class); + DoctrineType::overrideType(Types::DATETIME_MUTABLE, DateTimeImmutableType::class); + } + + protected function tearDown(): void + { + parent::tearDown(); + + DoctrineType::overrideType(Types::DATE_MUTABLE, DateType::class); + DoctrineType::overrideType(Types::DATETIME_MUTABLE, \Doctrine\DBAL\Types\DateTimeType::class); + } + + public function testShouldHandleStandardBehavior(): void + { + $sport = new ArticleCarbon(); + $sport->setTitle('Sport'); + $sport->setBody('Sport article body.'); + + $sportComment = new CommentCarbon(); + $sportComment->setMessage('hello'); + $sportComment->setArticle($sport); + $sportComment->setStatus(0); + + $author = new Author(); + $author->setName('Original author'); + $author->setEmail('original@author.dev'); + + $sport->setAuthor($author); + + $this->em->persist($sport); + $this->em->persist($sportComment); + $this->em->flush(); + + /** @var ArticleCarbon $sport */ + $sport = $this->em->getRepository(ArticleCarbon::class)->findOneBy(['title' => 'Sport']); + static::assertInstanceOf(CarbonImmutable::class, $su = $sport->getUpdated(), 'Type DATETIME_MUTABLE should become CarbonImmutable'); + static::assertInstanceOf(Carbon::class, $sc = $sport->getCreated(), 'Type DATE_MUTABLE should become Carbon'); + + static::assertNull($sport->getContentChanged()); + static::assertNull($sport->getPublished()); + static::assertNull($sport->getAuthorChanged()); + + $author = $sport->getAuthor(); + $author->setName('New author'); + $sport->setAuthor($author); + + /** @var CommentCarbon $sportComment */ + $sportComment = $this->em->getRepository(CommentCarbon::class)->findOneBy(['message' => 'hello']); + static::assertInstanceOf(\DateTime::class, $sportComment->getModified(), 'Type TIME_MUTABLE should stay DateTime'); + + static::assertNotNull($sportComment->getModified()); + static::assertNull($sportComment->getClosed()); + + $sportComment->setStatus(1); + $published = new Type(); + $published->setTitle('Published'); + + $sport->setType($published); + $this->em->persist($sport); + $this->em->persist($published); + $this->em->persist($sportComment); + $this->em->flush(); + + $sportComment = $this->em->getRepository(CommentCarbon::class)->findOneBy(['message' => 'hello']); + static::assertInstanceOf(CarbonImmutable::class, $scc = $sportComment->getClosed(), 'Type DATETIME_MUTABLE should become CarbonImmutable'); + static::assertInstanceOf(CarbonImmutable::class, $sp = $sport->getPublished(), 'Type DATETIME_MUTABLE should become CarbonImmutable'); + static::assertInstanceOf(CarbonImmutable::class, $sa = $sport->getAuthorChanged(), 'Type DATETIME_MUTABLE should become CarbonImmutable'); + + $sport->setTitle('Updated'); + $this->em->persist($sport); + $this->em->persist($published); + $this->em->persist($sportComment); + $this->em->flush(); + + static::assertSame($sport->getCreated(), $sc, 'Date created should remain same after update'); + static::assertNotSame($su2 = $sport->getUpdated(), $su, 'Date updated should change after update'); + static::assertInstanceOf(CarbonImmutable::class, $sport->getUpdated(), 'Type DATETIME_MUTABLE should become CarbonImmutable'); + static::assertSame($sport->getPublished(), $sp, 'Date published should remain the same after update'); + static::assertNotSame($scc2 = $sport->getContentChanged(), $scc, 'Content must have changed after update'); + static::assertInstanceOf(CarbonImmutable::class, $sport->getContentChanged(), 'Type DATETIME_MUTABLE should become CarbonImmutable'); + static::assertSame($sport->getAuthorChanged(), $sa, 'Author should remain same after update'); + + $author = $sport->getAuthor(); + $author->setName('Third author'); + $sport->setAuthor($author); + + $sport->setBody('Body updated'); + $this->em->persist($sport); + $this->em->persist($published); + $this->em->persist($sportComment); + $this->em->flush(); + + static::assertSame($sport->getCreated(), $sc, 'Date created should remain same after update'); + static::assertNotSame($sport->getUpdated(), $su2, 'Date updated should change after update'); + static::assertSame($sport->getPublished(), $sp, 'Date published should remain the same after update'); + static::assertNotSame($sport->getContentChanged(), $scc2, 'Content must have changed after update'); + static::assertNotSame($sport->getAuthorChanged(), $sa, 'Author must have changed after update'); + } + + protected function getUsedEntityFixtures(): array + { + return [ + ArticleCarbon::class, + CommentCarbon::class, + Type::class, + ]; + } +} diff --git a/tests/Gedmo/Timestampable/ChangeTest.php b/tests/Gedmo/Timestampable/ChangeTest.php index a40a121397..192ea84a1f 100644 --- a/tests/Gedmo/Timestampable/ChangeTest.php +++ b/tests/Gedmo/Timestampable/ChangeTest.php @@ -1,28 +1,38 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable; + +use Doctrine\Common\EventArgs; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Timestampable\Fixture\TitledArticle; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\AbstractTrackingListener; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; +use Gedmo\Tests\Timestampable\Fixture\TitledArticle; +use Gedmo\Tests\Tool\BaseTestCaseORM; use Gedmo\Timestampable\Mapping\Event\TimestampableAdapter; -use Doctrine\Common\EventArgs; /** * These are tests for Timestampable behavior * * @author Ivan Borzenkov - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ChangeTest extends BaseTestCaseORM +final class ChangeTest extends BaseTestCaseORM { - const FIXTURE = "Timestampable\\Fixture\\TitledArticle"; - + /** + * @var TimestampableListenerStub + */ protected $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -32,35 +42,35 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($this->listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testChange() + public function testChange(): void { $test = new TitledArticle(); $test->setTitle('Test'); $test->setText('Test'); $test->setState('Open'); - $currentDate = new \DateTime('now'); + $currentDate = new \DateTime(); $this->listener->eventAdapter->setDateValue($currentDate); $this->em->persist($test); $this->em->flush(); $this->em->clear(); - $test = $this->em->getRepository(self::FIXTURE)->findOneByTitle('Test'); + $test = $this->em->getRepository(TitledArticle::class)->findOneBy(['title' => 'Test']); $test->setTitle('New Title'); $test->setState('Closed'); $this->em->persist($test); $this->em->flush(); $this->em->clear(); - //Changed - $this->assertEquals( + // Changed + static::assertSame( $currentDate->format('Y-m-d H:i:s'), $test->getChtitle()->format('Y-m-d H:i:s') ); - $this->assertEquals( + static::assertSame( $currentDate->format('Y-m-d H:i:s'), $test->getClosed()->format('Y-m-d H:i:s') ); @@ -68,59 +78,68 @@ public function testChange() $anotherDate = \DateTime::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00'); $this->listener->eventAdapter->setDateValue($anotherDate); - $test = $this->em->getRepository(self::FIXTURE)->findOneByTitle('New Title'); + $test = $this->em->getRepository(TitledArticle::class)->findOneBy(['title' => 'New Title']); $test->setText('New Text'); $test->setState('Open'); $this->em->persist($test); $this->em->flush(); $this->em->clear(); - //Not Changed - $this->assertEquals( + // Not Changed + static::assertSame( $currentDate->format('Y-m-d H:i:s'), $test->getChtitle()->format('Y-m-d H:i:s') ); - $this->assertEquals( + static::assertSame( $currentDate->format('Y-m-d H:i:s'), $test->getClosed()->format('Y-m-d H:i:s') ); - $test = $this->em->getRepository(self::FIXTURE)->findOneByTitle('New Title'); + $test = $this->em->getRepository(TitledArticle::class)->findOneBy(['title' => 'New Title']); $test->setState('Published'); $this->em->persist($test); $this->em->flush(); $this->em->clear(); - //Changed - $this->assertEquals( + // Changed + static::assertSame( $anotherDate->format('Y-m-d H:i:s'), $test->getClosed()->format('Y-m-d H:i:s') ); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::FIXTURE, - ); + return [ + TitledArticle::class, + ]; } } -class EventAdapterORMStub extends BaseAdapterORM implements TimestampableAdapter +final class EventAdapterORMStub extends BaseAdapterORM implements TimestampableAdapter { - protected $dateTime; + private ?\DateTime $dateTime = null; - public function setDateValue(\DateTime $dateTime) + public function setDateValue(\DateTime $dateTime): void { $this->dateTime = $dateTime; } - public function getDateValue($meta, $field) + /** + * @param ClassMetadata $meta + */ + public function getDateValue($meta, $field): ?\DateTime { return $this->dateTime; } } -class TimestampableListenerStub extends TimestampableListener +/** + * @phpstan-extends AbstractTrackingListener + */ +final class TimestampableListenerStub extends AbstractTrackingListener { + /** + * @var EventAdapterORMStub + */ public $eventAdapter; protected function getEventAdapter(EventArgs $args) @@ -129,4 +148,18 @@ protected function getEventAdapter(EventArgs $args) return $this->eventAdapter; } + + /** + * @param ClassMetadata $meta + * @param EventAdapterORMStub $eventAdapter + */ + protected function getFieldValue($meta, $field, $eventAdapter) + { + return $eventAdapter->getDateValue($meta, $field); + } + + protected function getNamespace() + { + return 'Gedmo\Timestampable'; + } } diff --git a/tests/Gedmo/Timestampable/Fixture/Article.php b/tests/Gedmo/Timestampable/Fixture/Article.php index be8f78e800..ed1fd49314 100644 --- a/tests/Gedmo/Timestampable/Fixture/Article.php +++ b/tests/Gedmo/Timestampable/Fixture/Article.php @@ -1,158 +1,254 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Timestampable\Timestampable; /** * @ORM\Entity */ +#[ORM\Entity] class Article implements Timestampable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** * @ORM\Column(name="body", type="string") */ - private $body; + #[ORM\Column(name: 'body', type: Types::STRING)] + private ?string $body = null; /** - * @ORM\OneToMany(targetEntity="Timestampable\Fixture\Comment", mappedBy="article") + * @var Collection + * + * @ORM\OneToMany(targetEntity="Gedmo\Tests\Timestampable\Fixture\Comment", mappedBy="article") */ + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article')] private $comments; /** - * @var datetime $created - * + * @ORM\Embedded(class="Gedmo\Tests\Timestampable\Fixture\Author") + */ + #[ORM\Embedded(class: Author::class)] + private ?Author $author = null; + + /** * @Gedmo\Timestampable(on="create") + * * @ORM\Column(name="created", type="date") */ - private $created; + #[Gedmo\Timestampable(on: 'create')] + #[ORM\Column(name: 'created', type: Types::DATE_MUTABLE)] + private ?\DateTime $created = null; /** - * @var datetime $updated - * * @ORM\Column(name="updated", type="datetime") + * * @Gedmo\Timestampable */ - private $updated; + #[ORM\Column(name: 'updated', type: Types::DATETIME_MUTABLE)] + #[Gedmo\Timestampable] + private ?\DateTime $updated = null; /** - * @var datetime $published - * * @ORM\Column(name="published", type="datetime", nullable=true) + * * @Gedmo\Timestampable(on="change", field="type.title", value="Published") */ - private $published; + #[ORM\Column(name: 'published', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'type.title', value: 'Published')] + private ?\DateTime $published = null; /** - * @var datetime $contentChanged - * * @ORM\Column(name="content_changed", type="datetime", nullable=true) + * * @Gedmo\Timestampable(on="change", field={"title", "body"}) */ - private $contentChanged; + #[ORM\Column(name: 'content_changed', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: ['title', 'body'])] + private ?\DateTime $contentChanged = null; + /** + * @var \DateTime|null + * + * @ORM\Column(name="author_changed", type="datetime", nullable=true) + * + * @Gedmo\Timestampable(on="change", field={"author.name", "author.email"}) + */ + #[ORM\Column(name: 'author_changed', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: ['author.name', 'author.email'])] + private $authorChanged; /** * @ORM\ManyToOne(targetEntity="Type", inversedBy="articles") */ - private $type; + #[ORM\ManyToOne(targetEntity: Type::class, inversedBy: 'articles')] + private ?Type $type = null; + + /** + * @ORM\Column(name="level", type="integer") + */ + #[ORM\Column(name: 'level', type: Types::INTEGER)] + private int $level = 0; - public function setType($type) + /** + * We use the value "10" as string here in order to check the behavior of `AbstractTrackingListener` + * + * @var \DateTimeInterface|null + * + * @ORM\Column(name="reached_relevant_level", type="datetime", nullable=true) + * + * @Gedmo\Timestampable(on="change", field="level", value="10") + */ + #[ORM\Column(name: 'reached_relevant_level', type: Types::DATE_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'level', value: '10')] + private $reachedRelevantLevel; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } + + public function setType(?Type $type): void { $this->type = $type; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setBody($body) + public function setBody(?string $body): void { $this->body = $body; } - public function getBody() + public function getBody(): ?string { return $this->body; } - public function addComment(Comment $comment) + public function addComment(Comment $comment): void { $comment->setArticle($this); $this->comments[] = $comment; } - public function getComments() + /** + * @return Collection + */ + public function getComments(): Collection { return $this->comments; } - /** - * Get created - * - * @return datetime $created - */ - public function getCreated() + public function getAuthor(): ?Author + { + return $this->author; + } + + public function setAuthor(Author $author): void + { + $this->author = $author; + } + + public function getCreated(): ?\DateTime { return $this->created; } - public function setCreated(\DateTime $created) + public function setCreated(\DateTime $created): void { $this->created = $created; } - public function getPublished() + public function getPublished(): ?\DateTime { return $this->published; } - public function setPublished(\DateTime $published) + public function setPublished(\DateTime $published): void { $this->published = $published; } - /** - * Get updated - * - * @return datetime $updated - */ - public function getUpdated() + public function getUpdated(): ?\DateTime { return $this->updated; } - public function setUpdated(\DateTime $updated) + public function setUpdated(\DateTime $updated): void { $this->updated = $updated; } - public function setContentChanged(\DateTime $contentChanged) + public function setContentChanged(\DateTime $contentChanged): void { $this->contentChanged = $contentChanged; } - public function getContentChanged() + public function getContentChanged(): ?\DateTime { return $this->contentChanged; } + + public function getAuthorChanged(): ?\DateTime + { + return $this->authorChanged; + } + + public function setLevel(int $level): void + { + $this->level = $level; + } + + public function getLevel(): int + { + return $this->level; + } + + public function getReachedRelevantLevel(): ?\DateTimeInterface + { + return $this->reachedRelevantLevel; + } } diff --git a/tests/Gedmo/Timestampable/Fixture/ArticleCarbon.php b/tests/Gedmo/Timestampable/Fixture/ArticleCarbon.php new file mode 100644 index 0000000000..56d0d2ff32 --- /dev/null +++ b/tests/Gedmo/Timestampable/Fixture/ArticleCarbon.php @@ -0,0 +1,263 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; + +use Carbon\Carbon; +use Carbon\CarbonImmutable; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Timestampable\Timestampable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class ArticleCarbon implements Timestampable +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; + + /** + * @ORM\Column(name="title", type="string", length=128) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; + + /** + * @ORM\Column(name="body", type="string") + */ + #[ORM\Column(name: 'body', type: Types::STRING)] + private ?string $body = null; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="Gedmo\Tests\Timestampable\Fixture\Comment", mappedBy="article") + */ + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article')] + private Collection $comments; + + /** + * @ORM\Embedded(class="Gedmo\Tests\Timestampable\Fixture\Author") + */ + #[ORM\Embedded(class: Author::class)] + private ?Author $author = null; + + /** + * @var \DateTime|Carbon|null + * + * @Gedmo\Timestampable(on="create") + * + * @ORM\Column(name="created", type="date") + */ + #[Gedmo\Timestampable(on: 'create')] + #[ORM\Column(name: 'created', type: Types::DATE_MUTABLE)] + private $created; + + /** + * @var \DateTime|CarbonImmutable|null + * + * @ORM\Column(name="updated", type="datetime") + * + * @Gedmo\Timestampable + */ + #[ORM\Column(name: 'updated', type: Types::DATETIME_MUTABLE)] + #[Gedmo\Timestampable] + private $updated; + + /** + * @var \DateTime|CarbonImmutable|null + * + * @ORM\Column(name="published", type="datetime", nullable=true) + * + * @Gedmo\Timestampable(on="change", field="type.title", value="Published") + */ + #[ORM\Column(name: 'published', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'type.title', value: 'Published')] + private $published; + + /** + * @var \DateTime|CarbonImmutable|null + * + * @ORM\Column(name="content_changed", type="datetime", nullable=true) + * + * @Gedmo\Timestampable(on="change", field={"title", "body"}) + */ + #[ORM\Column(name: 'content_changed', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: ['title', 'body'])] + private $contentChanged; + + /** + * @ORM\Column(name="author_changed", type="datetime", nullable=true) + * + * @Gedmo\Timestampable(on="change", field={"author.name", "author.email"}) + */ + #[ORM\Column(name: 'author_changed', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: ['author.name', 'author.email'])] + private ?CarbonImmutable $authorChanged = null; + + /** + * @ORM\ManyToOne(targetEntity="Type", inversedBy="articles") + */ + #[ORM\ManyToOne(targetEntity: Type::class, inversedBy: 'articles')] + private ?Type $type = null; + + /** + * @ORM\Column(name="level", type="integer") + */ + #[ORM\Column(name: 'level', type: Types::INTEGER)] + private int $level = 0; + + /** + * We use the value "10" as string here in order to check the behavior of `AbstractTrackingListener` + * + * @ORM\Column(name="reached_relevant_level", type="datetime", nullable=true) + * + * @Gedmo\Timestampable(on="change", field="level", value="10") + */ + #[ORM\Column(name: 'reached_relevant_level', type: Types::DATE_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'level', value: '10')] + private ?\DateTimeInterface $reachedRelevantLevel = null; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } + + public function setType(?Type $type): void + { + $this->type = $type; + } + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setBody(?string $body): void + { + $this->body = $body; + } + + public function getBody(): ?string + { + return $this->body; + } + + public function addComment(Comment $comment): void + { + $comment->setArticle($this); + $this->comments[] = $comment; + } + + /** + * @return Collection + */ + public function getComments(): Collection + { + return $this->comments; + } + + public function getAuthor(): ?Author + { + return $this->author; + } + + public function setAuthor(Author $author): void + { + $this->author = $author; + } + + public function getCreated(): ?Carbon + { + return $this->created; + } + + /** @param CarbonImmutable|\DateTime $created */ + public function setCreated($created): void + { + $this->created = $created; + } + + public function getPublished(): ?CarbonImmutable + { + return $this->published; + } + + /** @param CarbonImmutable|\DateTime $published */ + public function setPublished($published): void + { + $this->published = $published; + } + + public function getUpdated(): ?CarbonImmutable + { + return $this->updated; + } + + /** @param CarbonImmutable|\DateTime $updated */ + public function setUpdated($updated): void + { + $this->updated = $updated; + } + + /** @param CarbonImmutable|\DateTime $contentChanged */ + public function setContentChanged($contentChanged): void + { + $this->contentChanged = $contentChanged; + } + + public function getContentChanged(): ?CarbonImmutable + { + return $this->contentChanged; + } + + public function getAuthorChanged(): ?CarbonImmutable + { + return $this->authorChanged; + } + + public function setLevel(int $level): void + { + $this->level = $level; + } + + public function getLevel(): int + { + return $this->level; + } + + public function getReachedRelevantLevel(): ?\DateTimeInterface + { + return $this->reachedRelevantLevel; + } +} diff --git a/tests/Gedmo/Timestampable/Fixture/Attribute/TitledArticle.php b/tests/Gedmo/Timestampable/Fixture/Attribute/TitledArticle.php new file mode 100644 index 0000000000..b9586bcda4 --- /dev/null +++ b/tests/Gedmo/Timestampable/Fixture/Attribute/TitledArticle.php @@ -0,0 +1,117 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture\Attribute; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Timestampable\Timestampable; + +#[ORM\Entity] +class TitledArticle implements Timestampable +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; + + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; + + #[ORM\Column(name: 'text', type: Types::STRING, length: 128)] + private ?string $text = null; + + #[ORM\Column(name: 'state', type: Types::STRING, length: 128)] + private ?string $state = null; + + #[ORM\Column(name: 'chtext', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'text')] + private ?\DateTime $chText = null; + + #[ORM\Column(name: 'chtitle', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'title')] + private ?\DateTime $chTitle = null; + + #[ORM\Column(name: 'closed', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'state', value: ['Published', 'Closed'])] + private ?\DateTime $closed = null; + + public function setChText(\DateTime $chText): void + { + $this->chText = $chText; + } + + public function getChText(): ?\DateTime + { + return $this->chText; + } + + public function setChTitle(\DateTime $chTitle): void + { + $this->chTitle = $chTitle; + } + + public function getChTitle(): ?\DateTime + { + return $this->chTitle; + } + + public function setClosed(\DateTime $closed): void + { + $this->closed = $closed; + } + + public function getClosed(): ?\DateTime + { + return $this->closed; + } + + public function setId(int $id): void + { + $this->id = $id; + } + + public function getId(): ?int + { + return $this->id; + } + + public function setText(string $text): void + { + $this->text = $text; + } + + public function getText(): ?string + { + return $this->text; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setState(string $state): void + { + $this->state = $state; + } + + public function getState(): ?string + { + return $this->state; + } +} diff --git a/tests/Gedmo/Timestampable/Fixture/Author.php b/tests/Gedmo/Timestampable/Fixture/Author.php new file mode 100644 index 0000000000..51fea2cd24 --- /dev/null +++ b/tests/Gedmo/Timestampable/Fixture/Author.php @@ -0,0 +1,54 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Embeddable + */ +#[ORM\Embeddable] +class Author +{ + /** + * @ORM\Column(name="author_name", type="string", length=128, nullable=true) + */ + #[ORM\Column(name: 'author_name', type: Types::STRING, length: 128, nullable: true)] + private ?string $name = null; + + /** + * @ORM\Column(name="author_email", type="string", length=50, nullable=true) + */ + #[ORM\Column(name: 'author_email', type: Types::STRING, length: 50, nullable: true)] + private ?string $email = null; + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): void + { + $this->name = $name; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(?string $email): void + { + $this->email = $email; + } +} diff --git a/tests/Gedmo/Timestampable/Fixture/Comment.php b/tests/Gedmo/Timestampable/Fixture/Comment.php index 6f97285c59..2631fd345d 100644 --- a/tests/Gedmo/Timestampable/Fixture/Comment.php +++ b/tests/Gedmo/Timestampable/Fixture/Comment.php @@ -1,86 +1,120 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Timestampable\Timestampable; /** * @ORM\Entity */ +#[ORM\Entity] class Comment implements Timestampable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="message", type="text") */ - private $message; + #[ORM\Column(name: 'message', type: Types::TEXT)] + private ?string $message = null; /** - * @ORM\ManyToOne(targetEntity="Timestampable\Fixture\Article", inversedBy="comments") + * @var Article|null + * + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Timestampable\Fixture\Article", inversedBy="comments") */ + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'comments')] private $article; /** * @ORM\Column(type="integer") */ - private $status; + #[ORM\Column(type: Types::INTEGER)] + private ?int $status = null; /** - * @var datetime $closed + * @var \DateTime|null * * @ORM\Column(name="closed", type="datetime", nullable=true) + * * @Gedmo\Timestampable(on="change", field="status", value=1) */ + #[ORM\Column(name: 'closed', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'status', value: 1)] private $closed; /** - * @var datetime $modified + * @var \DateTime|null * * @ORM\Column(name="modified", type="time") + * * @Gedmo\Timestampable(on="update") */ + #[ORM\Column(name: 'modified', type: Types::TIME_MUTABLE)] + #[Gedmo\Timestampable(on: 'update')] private $modified; - public function setArticle($article) + /** + * @param Article|ArticleCarbon $article + */ + public function setArticle(?Timestampable $article): void { $this->article = $article; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setStatus($status) + public function setStatus(?int $status): void { $this->status = $status; } - public function getStatus() + public function getStatus(): ?int { return $this->status; } - public function setMessage($message) + public function setMessage(?string $message): void { $this->message = $message; } - public function getMessage() + public function getMessage(): ?string { return $this->message; } - public function getModified() + public function getModified(): ?\DateTime { return $this->modified; } - public function getClosed() + public function getClosed(): ?\DateTime { return $this->closed; } diff --git a/tests/Gedmo/Timestampable/Fixture/CommentCarbon.php b/tests/Gedmo/Timestampable/Fixture/CommentCarbon.php new file mode 100644 index 0000000000..94057e5d3e --- /dev/null +++ b/tests/Gedmo/Timestampable/Fixture/CommentCarbon.php @@ -0,0 +1,117 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; + +use Carbon\CarbonImmutable; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Timestampable\Timestampable; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class CommentCarbon implements Timestampable +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="message", type="text") + */ + #[ORM\Column(name: 'message', type: Types::TEXT)] + private ?string $message = null; + + /** + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Timestampable\Fixture\ArticleCarbon", inversedBy="comments") + */ + #[ORM\ManyToOne(targetEntity: ArticleCarbon::class, inversedBy: 'comments')] + private ?ArticleCarbon $article = null; + + /** + * @ORM\Column(type="integer") + */ + #[ORM\Column(type: Types::INTEGER)] + private ?int $status = null; + + /** + * @var CarbonImmutable|null + * + * @ORM\Column(name="closed", type="datetime", nullable=true) + * + * @Gedmo\Timestampable(on="change", field="status", value=1) + */ + #[ORM\Column(name: 'closed', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'status', value: 1)] + private $closed; + + /** + * @var \DateTime|null + * + * @ORM\Column(name="modified", type="time") + * + * @Gedmo\Timestampable(on="update") + */ + #[ORM\Column(name: 'modified', type: Types::TIME_MUTABLE)] + #[Gedmo\Timestampable(on: 'update')] + private $modified; + + public function setArticle(?ArticleCarbon $article): void + { + $this->article = $article; + } + + public function getId(): ?int + { + return $this->id; + } + + public function setStatus(?int $status): void + { + $this->status = $status; + } + + public function getStatus(): ?int + { + return $this->status; + } + + public function setMessage(?string $message): void + { + $this->message = $message; + } + + public function getMessage(): ?string + { + return $this->message; + } + + public function getModified(): ?\DateTime + { + return $this->modified; + } + + public function getClosed(): ?CarbonImmutable + { + return $this->closed; + } +} diff --git a/tests/Gedmo/Timestampable/Fixture/Document/Article.php b/tests/Gedmo/Timestampable/Fixture/Document/Article.php index 3e4a00dc6b..09c779f0b6 100644 --- a/tests/Gedmo/Timestampable/Fixture/Document/Article.php +++ b/tests/Gedmo/Timestampable/Fixture/Document/Article.php @@ -1,140 +1,170 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; +use MongoDB\BSON\Timestamp; /** * @ODM\Document(collection="articles") */ +#[ODM\Document(collection: 'articles')] class Article { - /** @ODM\Id */ + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\ReferenceOne(targetDocument="Type") + * @ODM\ReferenceOne(targetDocument="Gedmo\Tests\Timestampable\Fixture\Document\Type") */ - private $type; + #[ODM\ReferenceOne(targetDocument: Type::class)] + private ?Type $type = null; /** - * @var timestamp $created + * @var int|Timestamp|null + * + * @ODM\Field(type="timestamp") * - * @ODM\Timestamp * @Gedmo\Timestampable(on="create") */ + #[Gedmo\Timestampable(on: 'create')] + #[ODM\Field(type: MongoDBType::TIMESTAMP)] private $created; /** - * @var date $updated + * @ODM\Field(type="date") * - * @ODM\Date * @Gedmo\Timestampable */ - private $updated; + #[Gedmo\Timestampable] + #[ODM\Field(type: MongoDBType::DATE)] + private ?\DateTime $updated = null; /** - * @var date $published + * @ODM\Field(type="date") * - * @ODM\Date * @Gedmo\Timestampable(on="change", field="type.title", value="Published") */ - private $published; + #[Gedmo\Timestampable(on: 'change', field: 'type.title', value: 'Published')] + #[ODM\Field(type: MongoDBType::DATE)] + private ?\DateTime $published = null; /** - * @var \DateTime - * @ODM\Date + * @ODM\Field(type="date") + * * @Gedmo\Timestampable(on="change", field="isReady", value=true) */ - private $ready; + #[Gedmo\Timestampable(on: 'change', field: 'isReady', value: true)] + #[ODM\Field(type: MongoDBType::DATE)] + private ?\DateTime $ready = null; /** - * @var bool - * @ODM\Boolean + * @ODM\Field(type="bool") */ - private $isReady = false; + #[ODM\Field(type: MongoDBType::BOOL)] + private bool $isReady = false; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } + /** + * @return int|Timestamp|null + */ public function getCreated() { return $this->created; } - public function getPublished() + public function getPublished(): \DateTime { return $this->published; } - public function getUpdated() + public function getUpdated(): \DateTime { return $this->updated; } - public function setType(Type $type) + public function setType(Type $type): void { $this->type = $type; } - public function getType() + public function getType(): ?Type { return $this->type; } - public function setCreated($created) + /** @param int|Timestamp|null $created */ + public function setCreated($created): void { $this->created = $created; } - public function setPublished(\DateTime $published) + public function setPublished(\DateTime $published): void { $this->published = $published; } - public function setUpdated(\DateTime $updated) + public function setUpdated(\DateTime $updated): void { $this->updated = $updated; } - public function setReady($ready) + public function setReady(?\DateTime $ready): self { $this->ready = $ready; return $this; } - public function getReady() + public function getReady(): ?\DateTime { return $this->ready; } - public function setIsReady($isReady) + public function setIsReady(bool $isReady): self { $this->isReady = $isReady; return $this; } - public function getIsReady() + public function getIsReady(): bool { return $this->isReady; } diff --git a/tests/Gedmo/Timestampable/Fixture/Document/Book.php b/tests/Gedmo/Timestampable/Fixture/Document/Book.php index ecb5632891..d683e3c81c 100644 --- a/tests/Gedmo/Timestampable/Fixture/Document/Book.php +++ b/tests/Gedmo/Timestampable/Fixture/Document/Book.php @@ -1,33 +1,49 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture\Document; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; -use Gedmo\Mapping\Annotation as Gedmo; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="books") */ +#[ODM\Document(collection: 'books')] class Book { /** - * @ODM\Id() + * @ODM\Id + * * @var string */ + #[ODM\Id] protected $id; /** - * @ODM\String() + * @ODM\Field(type="string") + * * @var string */ + #[ODM\Field(type: MongoDBType::STRING)] protected $title; /** - * @ODM\EmbedMany(targetDocument="Tag") - * @var Tag[]|Collection + * @ODM\EmbedMany(targetDocument="Gedmo\Tests\Timestampable\Fixture\Document\Tag") + * + * @var Collection */ + #[ODM\EmbedMany(targetDocument: Tag::class)] protected $tags; public function __construct() @@ -35,33 +51,23 @@ public function __construct() $this->tags = new ArrayCollection(); } - /** - * @return string - */ - public function getId() + public function getId(): string { return $this->id; } - /** - * @return string - */ - public function getTitle() + public function getTitle(): string { return $this->title; } - /** - * @param string $title - */ - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - /** - * @return Tag[]|Collection + * @return Collection */ public function getTags() { @@ -69,17 +75,14 @@ public function getTags() } /** - * @param Tag[] $tags + * @param Collection $tags */ - public function setTags(Collection $tags) + public function setTags(Collection $tags): void { $this->tags = $tags; } - /** - * @param Tag $tag - */ - public function addTag(Tag $tag) + public function addTag(Tag $tag): void { $this->tags->add($tag); } diff --git a/tests/Gedmo/Timestampable/Fixture/Document/Tag.php b/tests/Gedmo/Timestampable/Fixture/Document/Tag.php index bdd19f2a09..f2224a46a1 100644 --- a/tests/Gedmo/Timestampable/Fixture/Document/Tag.php +++ b/tests/Gedmo/Timestampable/Fixture/Document/Tag.php @@ -1,79 +1,82 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; use Gedmo\Mapping\Annotation as Gedmo; /** - * @ODM\EmbeddedDocument() + * @ODM\EmbeddedDocument */ +#[ODM\EmbeddedDocument] class Tag { /** - * @ODM\String() + * @ODM\Field(type="string") + * * @var string */ + #[ODM\Field(type: MongoDBType::STRING)] protected $name; /** - * @ODM\Date + * @ODM\Field(type="date") + * * @Gedmo\Timestampable(on="create") + * * @var \DateTime */ + #[Gedmo\Timestampable(on: 'create')] + #[ODM\Field(type: MongoDBType::DATE)] protected $created; /** - * @ODM\Date + * @ODM\Field(type="date") + * * @Gedmo\Timestampable + * * @var \DateTime */ + #[Gedmo\Timestampable] + #[ODM\Field(type: MongoDBType::DATE)] protected $updated; - /** - * @return string - */ - public function getName() + public function getName(): string { return $this->name; } - /** - * @param string $name - */ - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - /** - * @return \DateTime - */ - public function getCreated() + public function getCreated(): \DateTime { return $this->created; } - /** - * @param \DateTime $created - */ - public function setCreated(\DateTime $created) + public function setCreated(\DateTime $created): void { $this->created = $created; } - /** - * @return \DateTime - */ - public function getUpdated() + public function getUpdated(): \DateTime { return $this->updated; } - /** - * @param \DateTime $updated - */ - public function setUpdated(\DateTime $updated) + public function setUpdated(\DateTime $updated): void { $this->updated = $updated; } diff --git a/tests/Gedmo/Timestampable/Fixture/Document/Type.php b/tests/Gedmo/Timestampable/Fixture/Document/Type.php index e0ff1feb3b..c8b71df081 100644 --- a/tests/Gedmo/Timestampable/Fixture/Document/Type.php +++ b/tests/Gedmo/Timestampable/Fixture/Document/Type.php @@ -1,48 +1,66 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Types\Type as MongoDBType; /** * @ODM\Document(collection="types") */ +#[ODM\Document(collection: 'types')] class Type { - /** @ODM\Id */ + /** + * @var string|null + * + * @ODM\Id + */ + #[ODM\Id] private $id; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $title; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $title = null; /** - * @ODM\String + * @ODM\Field(type="string") */ - private $identifier; + #[ODM\Field(type: MongoDBType::STRING)] + private ?string $identifier = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getIdentifier() + public function getIdentifier(): ?string { return $this->identifier; } - public function setIdentifier($identifier) + public function setIdentifier(?string $identifier): void { $this->identifier = $identifier; } diff --git a/tests/Gedmo/Timestampable/Fixture/MappedSupperClass.php b/tests/Gedmo/Timestampable/Fixture/MappedSupperClass.php index 97144cc441..1118ac7e0b 100644 --- a/tests/Gedmo/Timestampable/Fixture/MappedSupperClass.php +++ b/tests/Gedmo/Timestampable/Fixture/MappedSupperClass.php @@ -1,84 +1,87 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** -* @ORM\MappedSuperclass -*/ + * @ORM\MappedSuperclass + */ +#[ORM\MappedSuperclass] class MappedSupperClass { /** - * @var integer $id - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - */ + * @var int|null + * + * @ORM\Column(name="id", type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + #[ORM\Column(name: 'id', type: Types::INTEGER)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] protected $id; /** - * @var string $locale - * - * @Gedmo\Locale - */ + * @var string|null + * + * @Gedmo\Locale + */ + #[Gedmo\Locale] protected $locale; /** - * @var string $title - * - * @Gedmo\Translatable - * @ORM\Column(name="name", type="string", length=255) - */ + * @var string|null + * + * @Gedmo\Translatable + * + * @ORM\Column(name="name", type="string", length=191) + */ + #[Gedmo\Translatable] + #[ORM\Column(name: 'name', type: Types::STRING, length: 191)] protected $name; /** - * @var \DateTime $createdAt - * - * @ORM\Column(name="created_at", type="datetime") - * @Gedmo\Timestampable(on="create") - */ + * @var \DateTime|null + * + * @ORM\Column(name="created_at", type="datetime") + * + * @Gedmo\Timestampable(on="create") + */ + #[ORM\Column(name: 'created_at', type: Types::DATETIME_MUTABLE)] + #[Gedmo\Timestampable(on: 'create')] protected $createdAt; /** - * Get id - * - * @return integer $id - * @codeCoverageIgnore - */ - public function getId() + * @codeCoverageIgnore + */ + public function getId(): ?int { return $this->id; } - /** - * Set name - * - * @param string $name - */ - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - /** - * Get name - * - * @return string $name - */ - public function getName() + public function getName(): ?string { return $this->name; } - /** - * Get createdAt - * - * @return \DateTime $createdAt - */ - public function getCreatedAt() + public function getCreatedAt(): ?\DateTime { return $this->createdAt; } diff --git a/tests/Gedmo/Timestampable/Fixture/SupperClassExtension.php b/tests/Gedmo/Timestampable/Fixture/SupperClassExtension.php index bf808bb04e..827a7f44a9 100644 --- a/tests/Gedmo/Timestampable/Fixture/SupperClassExtension.php +++ b/tests/Gedmo/Timestampable/Fixture/SupperClassExtension.php @@ -1,27 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; -use Gedmo\Mapping\Annotation as Gedmo; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class SupperClassExtension extends MappedSupperClass { /** * @ORM\Column(length=128) + * * @Gedmo\Translatable */ - private $title; + #[ORM\Column(length: 128)] + #[Gedmo\Translatable] + private ?string $title = null; - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Timestampable/Fixture/TitledArticle.php b/tests/Gedmo/Timestampable/Fixture/TitledArticle.php index 30d52fa823..edc19f2f55 100644 --- a/tests/Gedmo/Timestampable/Fixture/TitledArticle.php +++ b/tests/Gedmo/Timestampable/Fixture/TitledArticle.php @@ -1,13 +1,25 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Timestampable\Timestampable; /** * @ORM\Entity */ +#[ORM\Entity] class TitledArticle implements Timestampable { /** @@ -15,131 +27,122 @@ class TitledArticle implements Timestampable * @ORM\GeneratedValue * @ORM\Column(type="integer") */ - private $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; /** * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** * @ORM\Column(name="text", type="string", length=128) */ - private $text; + #[ORM\Column(name: 'text', type: Types::STRING, length: 128)] + private ?string $text = null; /** * @ORM\Column(name="state", type="string", length=128) */ - private $state; + #[ORM\Column(name: 'state', type: Types::STRING, length: 128)] + private ?string $state = null; /** - * @var \DateTime $updated - * * @ORM\Column(name="chtext", type="datetime", nullable=true) + * * @Gedmo\Timestampable(on="change", field="text") */ - private $chtext; + #[ORM\Column(name: 'chtext', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'text')] + private ?\DateTime $chText = null; /** - * @var \DateTime $chtitle - * * @ORM\Column(name="chtitle", type="datetime", nullable=true) + * * @Gedmo\Timestampable(on="change", field="title") */ - private $chtitle; + #[ORM\Column(name: 'chtitle', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'title')] + private ?\DateTime $chTitle = null; /** - * @var \DateTime $closed - * * @ORM\Column(name="closed", type="datetime", nullable=true) + * * @Gedmo\Timestampable(on="change", field="state", value={"Published", "Closed"}) */ - private $closed; + #[ORM\Column(name: 'closed', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Timestampable(on: 'change', field: 'state', value: ['Published', 'Closed'])] + private ?\DateTime $closed = null; - /** - * @param \DateTime $chtext - */ - public function setChtext($chtext) + public function setChText(\DateTime $chText): void { - $this->chtext = $chtext; + $this->chText = $chText; } - /** - * @return \DateTime - */ - public function getChtext() + public function getChText(): \DateTime { - return $this->chtext; + return $this->chText; } - /** - * @param \DateTime $chtitle - */ - public function setChtitle($chtitle) + public function setChTitle(\DateTime $chTitle): void { - $this->chtitle = $chtitle; + $this->chTitle = $chTitle; } - /** - * @return \DateTime - */ - public function getChtitle() + public function getChTitle(): \DateTime { - return $this->chtitle; + return $this->chTitle; } - /** - * @param \DateTime $closed - */ - public function setClosed($closed) + public function setClosed(\DateTime $closed): void { $this->closed = $closed; } - /** - * @return \DateTime - */ - public function getClosed() + public function getClosed(): \DateTime { return $this->closed; } - public function setId($id) + public function setId(?int $id): void { $this->id = $id; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setText($text) + public function setText(?string $text): void { $this->text = $text; } - public function getText() + public function getText(): ?string { return $this->text; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setState($state) + public function setState(?string $state): void { $this->state = $state; } - public function getState() + public function getState(): ?string { return $this->state; } diff --git a/tests/Gedmo/Timestampable/Fixture/Type.php b/tests/Gedmo/Timestampable/Fixture/Type.php index b07b927854..4a77ef3fde 100644 --- a/tests/Gedmo/Timestampable/Fixture/Type.php +++ b/tests/Gedmo/Timestampable/Fixture/Type.php @@ -1,37 +1,69 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Type { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="type") */ - private $articles; + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'type')] + private Collection $articles; + + public function __construct() + { + $this->articles = new ArrayCollection(); + } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Timestampable/Fixture/UsingTrait.php b/tests/Gedmo/Timestampable/Fixture/UsingTrait.php index 154c07a5e1..44d14d3ffa 100644 --- a/tests/Gedmo/Timestampable/Fixture/UsingTrait.php +++ b/tests/Gedmo/Timestampable/Fixture/UsingTrait.php @@ -1,43 +1,61 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Timestampable\Traits\TimestampableEntity; /** * @ORM\Entity */ +#[ORM\Entity] class UsingTrait { - /** + /* * Hook timestampable behavior * updates createdAt, updatedAt fields */ use TimestampableEntity; /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(length: 128)] + private ?string $title = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Timestampable/Fixture/WithoutInterface.php b/tests/Gedmo/Timestampable/Fixture/WithoutInterface.php index d18f439e87..e1de7f07f1 100644 --- a/tests/Gedmo/Timestampable/Fixture/WithoutInterface.php +++ b/tests/Gedmo/Timestampable/Fixture/WithoutInterface.php @@ -1,55 +1,87 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class WithoutInterface { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(type="string", length=128) */ - private $title; + #[ORM\Column(type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var \DateTime|null + * * @Gedmo\Timestampable(on="create") + * * @ORM\Column(type="date") */ + #[Gedmo\Timestampable(on: 'create')] + #[ORM\Column(type: Types::DATE_MUTABLE)] private $created; /** + * @var \DateTime|null + * * @ORM\Column(type="datetime") + * * @Gedmo\Timestampable(on="update") */ + #[ORM\Column(type: Types::DATETIME_MUTABLE)] + #[Gedmo\Timestampable(on: 'update')] private $updated; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function getCreated() + public function getCreated(): ?\DateTime { return $this->created; } - public function getUpdated() + public function getUpdated(): ?\DateTime { return $this->updated; } diff --git a/tests/Gedmo/Timestampable/NoInterfaceTest.php b/tests/Gedmo/Timestampable/NoInterfaceTest.php index 2f4b625d61..8af81a9560 100644 --- a/tests/Gedmo/Timestampable/NoInterfaceTest.php +++ b/tests/Gedmo/Timestampable/NoInterfaceTest.php @@ -1,57 +1,63 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Timestampable\Fixture\WithoutInterface; +use Gedmo\Tests\Timestampable\Fixture\WithoutInterface; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Timestampable\TimestampableListener; /** * These are tests for Timestampable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class NoInterfaceTest extends BaseTestCaseORM +final class NoInterfaceTest extends BaseTestCaseORM { - const FIXTURE = "Timestampable\\Fixture\\WithoutInterface"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TimestampableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testTimestampableNoInterface() + public function testTimestampableNoInterface(): void { $test = new WithoutInterface(); $test->setTitle('Test'); - $date = new \DateTime('now'); + $date = new \DateTime(); $this->em->persist($test); $this->em->flush(); $this->em->clear(); - $test = $this->em->getRepository(self::FIXTURE)->findOneByTitle('Test'); - $this->assertEquals( + $test = $this->em->getRepository(WithoutInterface::class)->findOneBy(['title' => 'Test']); + static::assertSame( $date->format('Y-m-d 00:00:00'), $test->getCreated()->format('Y-m-d H:i:s') ); - $this->assertEquals( + static::assertSame( $date->format('Y-m-d H:i'), $test->getUpdated()->format('Y-m-d H:i') ); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::FIXTURE, - ); + return [ + WithoutInterface::class, + ]; } } diff --git a/tests/Gedmo/Timestampable/ProtectedPropertySupperclassTest.php b/tests/Gedmo/Timestampable/ProtectedPropertySupperclassTest.php index a0e592251f..21d31ad17b 100644 --- a/tests/Gedmo/Timestampable/ProtectedPropertySupperclassTest.php +++ b/tests/Gedmo/Timestampable/ProtectedPropertySupperclassTest.php @@ -1,26 +1,31 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Gedmo\Translatable\TranslatableListener; +use Gedmo\Tests\Timestampable\Fixture\SupperClassExtension; +use Gedmo\Tests\Tool\BaseTestCaseORM; use Gedmo\Timestampable\TimestampableListener; -use Timestampable\Fixture\SupperClassExtension; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for Timestampable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ProtectedPropertySupperclassTest extends BaseTestCaseORM +final class ProtectedPropertySupperclassTest extends BaseTestCaseORM { - const SUPERCLASS = "Timestampable\\Fixture\\SupperClassExtension"; - const TRANSLATION = "Gedmo\\Translatable\\Entity\\Translation"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -30,10 +35,10 @@ protected function setUp() $evm->addEventSubscriber($translatableListener); $evm->addEventSubscriber(new TimestampableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testProtectedProperty() + public function testProtectedProperty(): void { $test = new SupperClassExtension(); $test->setName('name'); @@ -43,18 +48,18 @@ public function testProtectedProperty() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($test); - $this->assertCount(0, $translations); + static::assertCount(0, $translations); - $this->assertNotNull($test->getCreatedAt()); + static::assertNotNull($test->getCreatedAt()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TRANSLATION, - self::SUPERCLASS, - ); + return [ + Translation::class, + SupperClassExtension::class, + ]; } } diff --git a/tests/Gedmo/Timestampable/TimestampableDocumentTest.php b/tests/Gedmo/Timestampable/TimestampableDocumentTest.php index ad64f8a63c..a3547f6518 100644 --- a/tests/Gedmo/Timestampable/TimestampableDocumentTest.php +++ b/tests/Gedmo/Timestampable/TimestampableDocumentTest.php @@ -1,44 +1,49 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Timestampable\Fixture\Document\Article; -use Timestampable\Fixture\Document\Type; +use Gedmo\Tests\Timestampable\Fixture\Document\Article; +use Gedmo\Tests\Timestampable\Fixture\Document\Type; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; +use Gedmo\Timestampable\TimestampableListener; /** * These are tests for Timestampable behavior ODM implementation * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TimestampableDocumentTest extends BaseTestCaseMongoODM +final class TimestampableDocumentTest extends BaseTestCaseMongoODM { - const ARTICLE = 'Timestampable\Fixture\Document\Article'; - const TYPE = 'Timestampable\Fixture\Document\Type'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TimestampableListener()); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); $this->populate(); } - public function testTimestampable() + public function testTimestampable(): void { - $repo = $this->dm->getRepository(self::ARTICLE); - $article = $repo->findOneByTitle('Timestampable Article'); + $repo = $this->dm->getRepository(Article::class); + $article = $repo->findOneBy(['title' => 'Timestampable Article']); $date = new \DateTime(); $now = time(); - $created = intval((string) $article->getCreated()); - $this->assertTrue($created > $now - 5 && $created < $now + 5); // 5 seconds interval if lag - $this->assertEquals( + $created = $article->getCreated()->getTimestamp(); + static::assertTrue($created > $now - 5 && $created < $now + 5); // 5 seconds interval if lag + static::assertSame( $date->format('Y-m-d H:i'), $article->getUpdated()->format('Y-m-d H:i') ); @@ -53,15 +58,15 @@ public function testTimestampable() $this->dm->flush(); $this->dm->clear(); - $article = $repo->findOneByTitle('Timestampable Article'); + $article = $repo->findOneBy(['title' => 'Timestampable Article']); $date = new \DateTime(); - $this->assertEquals( + static::assertSame( $date->format('Y-m-d H:i'), $article->getPublished()->format('Y-m-d H:i') ); } - public function testForcedValues() + public function testForcedValues(): void { $sport = new Article(); $sport->setTitle('sport forced'); @@ -73,13 +78,13 @@ public function testForcedValues() $this->dm->flush(); $this->dm->clear(); - $repo = $this->dm->getRepository(self::ARTICLE); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals( + $repo = $this->dm->getRepository(Article::class); + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame( $created, - (string) $sport->getCreated() + $sport->getCreated()->getTimestamp() ); - $this->assertEquals( + static::assertSame( '2000-01-01 12:00:00', $sport->getUpdated()->format('Y-m-d H:i:s') ); @@ -95,31 +100,28 @@ public function testForcedValues() $this->dm->flush(); $this->dm->clear(); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals( + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame( '2000-01-01 12:00:00', $sport->getPublished()->format('Y-m-d H:i:s') ); } - /** - * @test - */ - public function shouldHandleOnChangeWithBooleanValue() + public function testShouldHandleOnChangeWithBooleanValue(): void { - $repo = $this->dm->getRepository(self::ARTICLE); - $article = $repo->findOneByTitle('Timestampable Article'); + $repo = $this->dm->getRepository(Article::class); + $article = $repo->findOneBy(['title' => 'Timestampable Article']); - $this->assertNull($article->getReady()); + static::assertNull($article->getReady()); $article->setIsReady(true); $this->dm->persist($article); $this->dm->flush(); - $this->assertNotNull($article->getReady()); + static::assertNotNull($article->getReady()); } - private function populate() + private function populate(): void { $art0 = new Article(); $art0->setTitle('Timestampable Article'); diff --git a/tests/Gedmo/Timestampable/TimestampableEmbeddedDocumentTest.php b/tests/Gedmo/Timestampable/TimestampableEmbeddedDocumentTest.php index d975f1fc22..756d65f08c 100644 --- a/tests/Gedmo/Timestampable/TimestampableEmbeddedDocumentTest.php +++ b/tests/Gedmo/Timestampable/TimestampableEmbeddedDocumentTest.php @@ -1,46 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable; use Doctrine\Common\EventManager; -use Timestampable\Fixture\Document\Book; -use Timestampable\Fixture\Document\Tag; -use Tool\BaseTestCaseMongoODM; +use Gedmo\Tests\Timestampable\Fixture\Document\Book; +use Gedmo\Tests\Timestampable\Fixture\Document\Tag; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; +use Gedmo\Timestampable\TimestampableListener; /** * These are tests for Timestampable behavior ODM implementation * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TimestampableEmbeddedDocumentTest extends BaseTestCaseMongoODM +final class TimestampableEmbeddedDocumentTest extends BaseTestCaseMongoODM { - const BOOK = 'Timestampable\Fixture\Document\Book'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TimestampableListener()); - $this->getMockDocumentManager($evm); - } - - /** - * Test that no php notice is triggered while processing timestampable properties of embedded document - */ - public function testPersistOnlyEmbeddedDocument() - { - $tag = new Tag(); - $tag->setName('cats'); - - $this->dm->persist($tag); - $this->dm->flush(); - $this->dm->clear(); + $this->getDefaultDocumentManager($evm); } - public function testPersistEmbeddedDocumentWithParent() + public function testPersistEmbeddedDocumentWithParent(): void { $tag1 = new Tag(); $tag1->setName('cats'); @@ -57,30 +50,30 @@ public function testPersistEmbeddedDocumentWithParent() $this->dm->flush(); $this->dm->clear(); - $repo = $this->dm->getRepository(self::BOOK); + $repo = $this->dm->getRepository(Book::class); - $bookFromRepo = $repo->findOneByTitle('Cats & Dogs'); + $bookFromRepo = $repo->findOneBy(['title' => 'Cats & Dogs']); - $this->assertNotNull($bookFromRepo); + static::assertNotNull($bookFromRepo); $date = new \DateTime(); - $this->assertEquals( + static::assertSame( $date->format('Y-m-d H:i'), $book->getTags()->get(0)->getCreated()->format('Y-m-d H:i') ); - $this->assertEquals( + static::assertSame( $date->format('Y-m-d H:i'), $book->getTags()->get(1)->getCreated()->format('Y-m-d H:i') ); - $this->assertEquals( + static::assertSame( $date->format('Y-m-d H:i'), $book->getTags()->get(0)->getUpdated()->format('Y-m-d H:i') ); - $this->assertEquals( + static::assertSame( $date->format('Y-m-d H:i'), $book->getTags()->get(1)->getUpdated()->format('Y-m-d H:i') ); diff --git a/tests/Gedmo/Timestampable/TimestampableTest.php b/tests/Gedmo/Timestampable/TimestampableTest.php index 12a1b384a5..593c0f5b3b 100644 --- a/tests/Gedmo/Timestampable/TimestampableTest.php +++ b/tests/Gedmo/Timestampable/TimestampableTest.php @@ -1,42 +1,54 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Timestampable\Fixture\Article; -use Timestampable\Fixture\Comment; -use Timestampable\Fixture\Type; +use Gedmo\Tests\Clock; +use Gedmo\Tests\Timestampable\Fixture\Article; +use Gedmo\Tests\Timestampable\Fixture\Author; +use Gedmo\Tests\Timestampable\Fixture\Comment; +use Gedmo\Tests\Timestampable\Fixture\Type; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Timestampable\TimestampableListener; /** * These are tests for Timestampable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TimestampableTest extends BaseTestCaseORM +final class TimestampableTest extends BaseTestCaseORM { - const ARTICLE = "Timestampable\\Fixture\\Article"; - const COMMENT = "Timestampable\\Fixture\\Comment"; - const TYPE = "Timestampable\\Fixture\\Type"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); + $listener = new TimestampableListener(); + $listener->setClock(new Clock()); + $evm = new EventManager(); - $evm->addEventSubscriber(new TimestampableListener()); + $evm->addEventSubscriber($listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } /** * issue #1255 - * @test */ - function shouldHandleDetatchedAndMergedBackEntities() + public function testShouldHandleDetatchedAndMergedBackEntities(): void { + if (!method_exists($this->em, 'merge')) { + static::markTestSkipped('Test covers behavior with EntityManager::merge() which does not exist on ORM 3'); + } + $sport = new Article(); $sport->setTitle('Sport'); $sport->setBody('Sport article body.'); @@ -47,15 +59,18 @@ function shouldHandleDetatchedAndMergedBackEntities() $this->em->persist($newSport); $this->em->flush(); - $this->assertNotNull($newSport->getUpdated()); + static::assertNotNull($newSport->getUpdated()); } /** * issue #1255 - * @test */ - function shouldHandleDetatchedAndMergedBackEntitiesAfterPersist() + public function testShouldHandleDetatchedAndMergedBackEntitiesAfterPersist(): void { + if (!method_exists($this->em, 'merge')) { + static::markTestSkipped('Test covers behavior with EntityManager::merge() which does not exist on ORM 3'); + } + $sport = new Article(); $sport->setTitle('Sport'); $sport->setBody('Sport article body.'); @@ -70,19 +85,16 @@ function shouldHandleDetatchedAndMergedBackEntitiesAfterPersist() $this->em->persist($newSport); $this->em->flush(); - $this->assertSame($newSport->getUpdated(), $updated, "There was no change, should remain the same"); + static::assertSame($newSport->getUpdated(), $updated, 'There was no change, should remain the same'); $newSport->setTitle('updated'); $this->em->persist($newSport); $this->em->flush(); - $this->assertNotSame($newSport->getUpdated(), $updated, "There was a change, should not remain the same"); + static::assertNotSame($newSport->getUpdated(), $updated, 'There was a change, should not remain the same'); } - /** - * @test - */ - function shouldHandleStandardBehavior() + public function testShouldHandleStandardBehavior(): void { $sport = new Article(); $sport->setTitle('Sport'); @@ -93,19 +105,30 @@ function shouldHandleStandardBehavior() $sportComment->setArticle($sport); $sportComment->setStatus(0); + $author = new Author(); + $author->setName('Original author'); + $author->setEmail('original@author.dev'); + + $sport->setAuthor($author); + $this->em->persist($sport); $this->em->persist($sportComment); $this->em->flush(); - $sport = $this->em->getRepository(self::ARTICLE)->findOneByTitle('Sport'); - $this->assertNotNull($sc = $sport->getCreated()); - $this->assertNotNull($su = $sport->getUpdated()); - $this->assertNull($sport->getContentChanged()); - $this->assertNull($sport->getPublished()); + $sport = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); + static::assertNotNull($sc = $sport->getCreated()); + static::assertNotNull($su = $sport->getUpdated()); + static::assertNull($sport->getContentChanged()); + static::assertNull($sport->getPublished()); + static::assertNull($sport->getAuthorChanged()); + + $author = $sport->getAuthor(); + $author->setName('New author'); + $sport->setAuthor($author); - $sportComment = $this->em->getRepository(self::COMMENT)->findOneByMessage('hello'); - $this->assertNotNull($scm = $sportComment->getModified()); - $this->assertNull($sportComment->getClosed()); + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertNotNull($sportComment->getModified()); + static::assertNull($sportComment->getClosed()); $sportComment->setStatus(1); $published = new Type(); @@ -117,9 +140,10 @@ function shouldHandleStandardBehavior() $this->em->persist($sportComment); $this->em->flush(); - $sportComment = $this->em->getRepository(self::COMMENT)->findOneByMessage('hello'); - $this->assertNotNull($scc = $sportComment->getClosed()); - $this->assertNotNull($sp = $sport->getPublished()); + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertNotNull($scc = $sportComment->getClosed()); + static::assertNotNull($sp = $sport->getPublished()); + static::assertNotNull($sa = $sport->getAuthorChanged()); $sport->setTitle('Updated'); $this->em->persist($sport); @@ -127,10 +151,15 @@ function shouldHandleStandardBehavior() $this->em->persist($sportComment); $this->em->flush(); - $this->assertSame($sport->getCreated(), $sc, "Date created should remain same after update"); - $this->assertNotSame($su2 = $sport->getUpdated(), $su, "Date updated should change after update"); - $this->assertSame($sport->getPublished(), $sp, "Date published should remain the same after update"); - $this->assertNotSame($scc2 = $sport->getContentChanged(), $scc, "Content must have changed after update"); + static::assertSame($sport->getCreated(), $sc, 'Date created should remain same after update'); + static::assertNotSame($su2 = $sport->getUpdated(), $su, 'Date updated should change after update'); + static::assertSame($sport->getPublished(), $sp, 'Date published should remain the same after update'); + static::assertNotSame($scc2 = $sport->getContentChanged(), $scc, 'Content must have changed after update'); + static::assertSame($sport->getAuthorChanged(), $sa, 'Author should remain same after update'); + + $author = $sport->getAuthor(); + $author->setName('Third author'); + $sport->setAuthor($author); $sport->setBody('Body updated'); $this->em->persist($sport); @@ -138,16 +167,14 @@ function shouldHandleStandardBehavior() $this->em->persist($sportComment); $this->em->flush(); - $this->assertSame($sport->getCreated(), $sc, "Date created should remain same after update"); - $this->assertNotSame($sport->getUpdated(), $su2, "Date updated should change after update"); - $this->assertSame($sport->getPublished(), $sp, "Date published should remain the same after update"); - $this->assertNotSame($sport->getContentChanged(), $scc2, "Content must have changed after update"); + static::assertSame($sport->getCreated(), $sc, 'Date created should remain same after update'); + static::assertNotSame($sport->getUpdated(), $su2, 'Date updated should change after update'); + static::assertSame($sport->getPublished(), $sp, 'Date published should remain the same after update'); + static::assertNotSame($sport->getContentChanged(), $scc2, 'Content must have changed after update'); + static::assertNotSame($sport->getAuthorChanged(), $sa, 'Author must have changed after update'); } - /** - * @test - */ - function shouldBeAbleToForceDates() + public function testShouldBeAbleToForceDates(): void { $sport = new Article(); $sport->setTitle('sport forced'); @@ -159,17 +186,17 @@ function shouldBeAbleToForceDates() $this->em->persist($sport); $this->em->flush(); - $repo = $this->em->getRepository(self::ARTICLE); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals( + $repo = $this->em->getRepository(Article::class); + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame( '2000-01-01', $sport->getCreated()->format('Y-m-d') ); - $this->assertEquals( + static::assertSame( '2000-01-01 12:00:00', $sport->getUpdated()->format('Y-m-d H:i:s') ); - $this->assertEquals( + static::assertSame( '2000-01-01 12:00:00', $sport->getContentChanged()->format('Y-m-d H:i:s') ); @@ -183,8 +210,8 @@ function shouldBeAbleToForceDates() $this->em->persist($published); $this->em->flush(); - $sport = $repo->findOneByTitle('sport forced'); - $this->assertEquals( + $sport = $repo->findOneBy(['title' => 'sport forced']); + static::assertSame( '2000-01-01 12:00:00', $sport->getPublished()->format('Y-m-d H:i:s') ); @@ -192,10 +219,7 @@ function shouldBeAbleToForceDates() $this->em->clear(); } - /** - * @test - */ - function shouldSolveIssue767() + public function testShouldSolveIssue767(): void { $type = new Type(); $type->setTitle('Published'); @@ -204,8 +228,8 @@ function shouldSolveIssue767() $this->em->flush(); $this->em->clear(); - $type = $this->em->getReference(self::TYPE, $type->getId()); - $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $type); + $type = $this->em->getReference(Type::class, $type->getId()); + static::assertTrue($this->em->isUninitializedObject($type)); $art = new Article(); $art->setTitle('Art'); @@ -217,15 +241,54 @@ function shouldSolveIssue767() $art->setType($type); $this->em->flush(); // in v2.4.x will work on insert too - $this->assertNotNull($art->getPublished()); + static::assertNotNull($art->getPublished()); + } + + /** + * @see https://github.com/doctrine-extensions/DoctrineExtensions/issues/2367. + */ + public function testHandledTypes(): void + { + $timespampable = new Article(); + $timespampable->setTitle('My article'); + $timespampable->setBody('My article body.'); + + static::assertNull($timespampable->getReachedRelevantLevel()); + $timespampable->setLevel(8); + + $this->em->persist($timespampable); + $this->em->flush(); + + $repo = $this->em->getRepository(Article::class); + $found = $repo->findOneBy(['body' => 'My article body.']); + + static::assertNull($found->getReachedRelevantLevel()); + + $timespampable->setLevel(9); + + $this->em->persist($timespampable); + $this->em->flush(); + + $found = $repo->findOneBy(['body' => 'My article body.']); + + static::assertNull($found->getReachedRelevantLevel()); + + $timespampable->setLevel(10); + + $this->em->persist($timespampable); + $this->em->flush(); + + $found = $repo->findOneBy(['body' => 'My article body.']); + + static::assertInstanceOf(\DateTime::class, $found->getReachedRelevantLevel()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::COMMENT, - self::TYPE, - ); + return [ + Article::class, + Comment::class, + Type::class, + ]; } } diff --git a/tests/Gedmo/Timestampable/TraitUsageTest.php b/tests/Gedmo/Timestampable/TraitUsageTest.php index c48cb6a6d6..5a354e8020 100644 --- a/tests/Gedmo/Timestampable/TraitUsageTest.php +++ b/tests/Gedmo/Timestampable/TraitUsageTest.php @@ -1,40 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Timestampable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Timestampable\Fixture\UsingTrait; +use Gedmo\Tests\Timestampable\Fixture\UsingTrait; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Timestampable\TimestampableListener; /** * These are tests for Timestampable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TraitUsageTest extends BaseTestCaseORM +final class TraitUsageTest extends BaseTestCaseORM { - const TARGET = "Timestampable\\Fixture\\UsingTrait"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); - if (version_compare(PHP_VERSION, '5.4.0') < 0) { - $this->markTestSkipped('PHP >= 5.4 version required for this test.'); - } - $evm = new EventManager(); $evm->addEventSubscriber(new TimestampableListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldTimestampUsingTrait() + public function testShouldTimestampUsingTrait(): void { $sport = new UsingTrait(); $sport->setTitle('Sport'); @@ -42,24 +41,21 @@ public function shouldTimestampUsingTrait() $this->em->persist($sport); $this->em->flush(); - $this->assertNotNull($sport->getCreatedAt()); - $this->assertNotNull($sport->getUpdatedAt()); + static::assertNotNull($sport->getCreatedAt()); + static::assertNotNull($sport->getUpdatedAt()); } - /** - * @test - */ - public function traitMethodthShouldReturnObject() + public function testTraitMethodthShouldReturnObject(): void { $sport = new UsingTrait(); - $this->assertInstanceOf('Timestampable\Fixture\UsingTrait', $sport->setCreatedAt(new \DateTime())); - $this->assertInstanceOf('Timestampable\Fixture\UsingTrait', $sport->setUpdatedAt(new \DateTime())); + static::assertInstanceOf(UsingTrait::class, $sport->setCreatedAt(new \DateTime())); + static::assertInstanceOf(UsingTrait::class, $sport->setUpdatedAt(new \DateTime())); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::TARGET, - ); + return [ + UsingTrait::class, + ]; } } diff --git a/tests/Gedmo/Tool/BaseTestCaseMongoODM.php b/tests/Gedmo/Tool/BaseTestCaseMongoODM.php index 077d90c4cb..1e9986a803 100644 --- a/tests/Gedmo/Tool/BaseTestCaseMongoODM.php +++ b/tests/Gedmo/Tool/BaseTestCaseMongoODM.php @@ -1,17 +1,31 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tool; -use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver; -use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\Common\EventManager; -use Doctrine\MongoDB\Connection; -use Doctrine\ODM\MongoDB\Repository\DefaultRepositoryFactory; -use Gedmo\Translatable\TranslatableListener; +use Doctrine\ODM\MongoDB\Configuration; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver; +use Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; +use Gedmo\Loggable\LoggableListener; use Gedmo\Sluggable\SluggableListener; -use Gedmo\Timestampable\TimestampableListener; +use Gedmo\SoftDeleteable\Filter\ODM\SoftDeleteableFilter; use Gedmo\SoftDeleteable\SoftDeleteableListener; -use Gedmo\Loggable\LoggableListener; +use Gedmo\Timestampable\TimestampableListener; +use Gedmo\Translatable\TranslatableListener; +use MongoDB\Client; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; /** * Base test case contains common mock objects @@ -19,188 +33,149 @@ * ORM object manager * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -abstract class BaseTestCaseMongoODM extends \PHPUnit_Framework_TestCase +abstract class BaseTestCaseMongoODM extends TestCase { /** - * @var DocumentManager + * @var DocumentManager|null */ protected $dm; - /** - * {@inheritdoc} - */ - protected function setUp() + protected function setUp(): void { - if (!class_exists('Mongo')) { - $this->markTestSkipped('Missing Mongo extension.'); + if (!extension_loaded('mongodb')) { + static::markTestSkipped('Missing Mongo extension.'); } } - /** - * {@inheritdoc} - */ - protected function tearDown() + protected function tearDown(): void { - if ($this->dm) { - foreach ($this->dm->getDocumentDatabases() as $db) { - foreach ($db->listCollections() as $collection) { - $collection->drop(); - } - } - $this->dm->getConnection()->close(); - $this->dm = null; + if (null === $this->dm) { + return; } + + foreach ($this->dm->getDocumentDatabases() as $documentDatabase) { + $documentDatabase->drop(); + } + + $this->dm = null; } /** - * DocumentManager mock object together with - * annotation mapping driver and database - * - * @param EventManager $evm - * - * @return DocumentManager + * DocumentManager mock object together with annotation mapping driver and database. */ - protected function getMockDocumentManager(EventManager $evm = null, $config = null) + protected function getMockDocumentManager(?EventManager $evm = null, ?Configuration $config = null): DocumentManager { - $conn = new Connection(); + $client = new Client($_ENV['MONGODB_SERVER'], [], ['typeMap' => DocumentManager::CLIENT_TYPEMAP]); - $config = $config ? $config : $this->getMockAnnotatedConfig(); + $config ??= $this->getMockAnnotatedConfig(); + $evm ??= $this->getEventManager(); - try { - $this->dm = DocumentManager::create($conn, $config, $evm ?: $this->getEventManager()); - $this->dm->getConnection()->connect(); - } catch (\MongoException $e) { - $this->markTestSkipped('Doctrine MongoDB ODM failed to connect'); - } + return $this->dm = DocumentManager::create($client, $config, $evm); + } - return $this->dm; + protected function getDefaultDocumentManager(?EventManager $evm = null): DocumentManager + { + return $this->getMockDocumentManager($evm, $this->getDefaultConfiguration()); } /** * DocumentManager mock object with * annotation mapping driver - * - * @param EventManager $evm - * - * @return DocumentManager */ - protected function getMockMappedDocumentManager(EventManager $evm = null, $config = null) + protected function getMockMappedDocumentManager(?EventManager $evm = null, ?Configuration $config = null): DocumentManager { - $conn = $this->getMock('Doctrine\\MongoDB\\Connection'); + $conn = static::createStub(Client::class); - $config = $config ? $config : $this->getMockAnnotatedConfig(); + $config ??= $this->getMockAnnotatedConfig(); - $this->dm = DocumentManager::create($conn, $config, $evm ?: $this->getEventManager()); + $this->dm = DocumentManager::create($conn, $config, $evm ?? $this->getEventManager()); return $this->dm; } /** * Creates default mapping driver - * - * @return \Doctrine\ORM\Mapping\Driver\Driver */ - protected function getMetadataDriverImplementation() + protected function getMetadataDriverImplementation(): MappingDriver { - return new AnnotationDriver($_ENV['annotation_reader']); - } - - /** - * Build event manager - * - * @return EventManager - */ - private function getEventManager() - { - $evm = new EventManager(); - $evm->addEventSubscriber(new SluggableListener()); - $evm->addEventSubscriber(new LoggableListener()); - $evm->addEventSubscriber(new TranslatableListener()); - $evm->addEventSubscriber(new TimestampableListener()); - $evm->addEventSubscriber(new SoftDeleteableListener()); + if (PHP_VERSION_ID >= 80000) { + return new AttributeDriver(); + } - return $evm; + return new AnnotationDriver($_ENV['annotation_reader']); } /** * Get annotation mapping configuration - * - * @return Doctrine\ORM\Configuration */ - protected function getMockAnnotatedConfig() + protected function getMockAnnotatedConfig(): Configuration { - $config = $this->getMock('Doctrine\\ODM\\MongoDB\\Configuration'); - - $config->expects($this->any()) - ->method('getFilterClassName') - ->will($this->returnValue('Gedmo\\SoftDeleteable\\Filter\\ODM\\SoftDeleteableFilter')); - - $config->expects($this->any()) - ->method('getFilterParameters') - ->will($this->returnValue(array())); - - $config->expects($this->once()) - ->method('getProxyDir') - ->will($this->returnValue(__DIR__.'/../../temp')); - - $config->expects($this->once()) - ->method('getProxyNamespace') - ->will($this->returnValue('Proxy')); + $config = new Configuration(); - $config->expects($this->once()) - ->method('getHydratorDir') - ->will($this->returnValue(__DIR__.'/../../temp')); - - $config->expects($this->once()) - ->method('getHydratorNamespace') - ->will($this->returnValue('Hydrator')); - - $config->expects($this->any()) - ->method('getDefaultDB') - ->will($this->returnValue('gedmo_extensions_test')); - - $config->expects($this->once()) - ->method('getAutoGenerateProxyClasses') - ->will($this->returnValue(true)); + /** @phpstan-ignore-next-line function.alreadyNarrowedType */ + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'setUseNativeLazyObject')) { + $config->setUseNativeLazyObject(true); + } - $config->expects($this->once()) - ->method('getAutoGenerateHydratorClasses') - ->will($this->returnValue(true)); + $config->addFilter('softdeleteable', SoftDeleteableFilter::class); + $config->setProxyDir(TESTS_TEMP_DIR); + $config->setHydratorDir(TESTS_TEMP_DIR); + $config->setProxyNamespace('Proxy'); + $config->setHydratorNamespace('Hydrator'); + $config->setDefaultDB('gedmo_extensions_test'); + $config->setAutoGenerateProxyClasses(Configuration::AUTOGENERATE_EVAL); + $config->setAutoGenerateHydratorClasses(Configuration::AUTOGENERATE_EVAL); + $config->setMetadataDriverImpl($this->getMetadataDriverImplementation()); + $config->setMetadataCache(new ArrayAdapter()); - $config->expects($this->once()) - ->method('getClassMetadataFactoryName') - ->will($this->returnValue('Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory')); + return $config; + } - $config - ->expects($this->any()) - ->method('getMongoCmd') - ->will($this->returnValue('$')) - ; + protected function getDefaultConfiguration(): Configuration + { + $config = new Configuration(); - $config - ->expects($this->any()) - ->method('getDefaultCommitOptions') - ->will($this->returnValue(array('safe' => true))) - ; + /** @phpstan-ignore-next-line function.alreadyNarrowedType */ + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'setUseNativeLazyObject')) { + $config->setUseNativeLazyObject(true); + } - $mappingDriver = $this->getMetadataDriverImplementation(); + $config->addFilter('softdeleteable', SoftDeleteableFilter::class); + $config->setProxyDir(TESTS_TEMP_DIR); + $config->setHydratorDir(TESTS_TEMP_DIR); + $config->setProxyNamespace('Proxy'); + $config->setHydratorNamespace('Hydrator'); + $config->setDefaultDB('gedmo_extensions_test'); + $config->setAutoGenerateProxyClasses(Configuration::AUTOGENERATE_EVAL); + $config->setAutoGenerateHydratorClasses(Configuration::AUTOGENERATE_EVAL); + $config->setMetadataDriverImpl($this->getMetadataDefaultDriverImplementation()); + $config->setMetadataCache(new ArrayAdapter()); - $config->expects($this->any()) - ->method('getMetadataDriverImpl') - ->will($this->returnValue($mappingDriver)); + return $config; + } - $config->expects($this->any()) - ->method('getRepositoryFactory') - ->will($this->returnValue(new DefaultRepositoryFactory())); + private function getMetadataDefaultDriverImplementation(): MappingDriver + { + if (PHP_VERSION_ID >= 80000 && class_exists(AttributeDriver::class)) { + return new AttributeDriver([]); + } - $config->expects($this->any()) - ->method('getDefaultRepositoryClassName') - ->will($this->returnValue('Doctrine\\ODM\\MongoDB\\DocumentRepository')); + return new AnnotationDriver($_ENV['annotation_reader']); + } + /** + * Build event manager + */ + private function getEventManager(): EventManager + { + $evm = new EventManager(); + $evm->addEventSubscriber(new SluggableListener()); + $evm->addEventSubscriber(new LoggableListener()); + $evm->addEventSubscriber(new TranslatableListener()); + $evm->addEventSubscriber(new TimestampableListener()); + $evm->addEventSubscriber(new SoftDeleteableListener()); - return $config; + return $evm; } } diff --git a/tests/Gedmo/Tool/BaseTestCaseOM.php b/tests/Gedmo/Tool/BaseTestCaseOM.php index bbb1c6d08a..90810c9820 100644 --- a/tests/Gedmo/Tool/BaseTestCaseOM.php +++ b/tests/Gedmo/Tool/BaseTestCaseOM.php @@ -1,28 +1,42 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tool; -// common use Doctrine\Common\EventManager; -use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; -// orm specific -use Doctrine\ORM\Mapping\DefaultQuoteStrategy; +use Doctrine\DBAL\DriverManager; +use Doctrine\ODM\MongoDB\Configuration; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver as AnnotationDriverODM; +use Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataFactory; use Doctrine\ORM\Mapping\DefaultNamingStrategy; +use Doctrine\ORM\Mapping\DefaultQuoteStrategy; use Doctrine\ORM\Mapping\Driver\AnnotationDriver as AnnotationDriverORM; -use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AttributeDriver as AttributeDriverORM; +use Doctrine\ORM\Repository\DefaultRepositoryFactory as DefaultRepositoryFactoryORM; use Doctrine\ORM\Tools\SchemaTool; -// odm specific -use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver as AnnotationDriverODM; -use Doctrine\ODM\MongoDB\DocumentManager; -use Doctrine\MongoDB\Connection; -// listeners -use Gedmo\Translatable\TranslatableListener; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; +use Gedmo\Loggable\LoggableListener; use Gedmo\Sluggable\SluggableListener; -use Gedmo\Tree\TreeListener; +use Gedmo\SoftDeleteable\Filter\ODM\SoftDeleteableFilter; use Gedmo\Timestampable\TimestampableListener; -use Gedmo\Loggable\LoggableListener; -use Doctrine\ORM\Repository\DefaultRepositoryFactory as DefaultRepositoryFactoryORM; -use Doctrine\ODM\MongoDB\Repository\DefaultRepositoryFactory as DefaultRepositoryFactoryODM; +use Gedmo\Translatable\TranslatableListener; +use Gedmo\Tree\TreeListener; +use MongoDB\Client; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; /** * Base test case contains common mock objects @@ -30,10 +44,8 @@ * test cases * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -abstract class BaseTestCaseOM extends \PHPUnit_Framework_TestCase +abstract class BaseTestCaseOM extends TestCase { /** * @var EventManager @@ -45,78 +57,59 @@ abstract class BaseTestCaseOM extends \PHPUnit_Framework_TestCase * * @var DocumentManager[] */ - private $dms = array(); + private array $dms = []; - /** - * {@inheritdoc} - */ - protected function setUp() + protected function setUp(): void { } - /** - * {@inheritdoc} - */ - protected function tearDown() + protected function tearDown(): void { - foreach ($this->dms as $dm) { - if ($dm) { - foreach ($dm->getDocumentDatabases() as $db) { - foreach ($db->listCollections() as $collection) { - $collection->drop(); - } - } - $dm->getConnection()->close(); - $dm = null; + foreach ($this->dms as $documentManager) { + foreach ($documentManager->getDocumentDatabases() as $documentDatabase) { + $documentDatabase->drop(); } } } /** - * DocumentManager mock object together with - * annotation mapping driver and database - * - * @param string $dbName - * @param MappingDriver $mappingDriver - * - * @return DocumentManager + * @param string[] $paths */ - protected function getMockDocumentManager($dbName, MappingDriver $mappingDriver = null) + protected function getMongoDBDriver(array $paths = []): MappingDriver { - if (!class_exists('Mongo')) { - $this->markTestSkipped('Missing Mongo extension.'); + if (PHP_VERSION_ID >= 80000 && class_exists(AttributeDriver::class)) { + return new AttributeDriver($paths); } - $conn = new Connection(); - $config = $this->getMockAnnotatedODMMongoDBConfig($dbName, $mappingDriver); - - $dm = null; - try { - $dm = DocumentManager::create($conn, $config, $this->getEventManager()); - $dm->getConnection()->connect(); - } catch (\MongoException $e) { - $this->markTestSkipped('Doctrine MongoDB ODM failed to connect'); + + return new AnnotationDriverODM($_ENV['annotation_reader'], $paths); + } + + /** + * @param string[] $paths + */ + protected function getORMDriver(array $paths = []): MappingDriver + { + if (PHP_VERSION_ID >= 80000) { + return new AttributeDriverORM($paths); } - return $dm; + return new AnnotationDriverORM($_ENV['annotation_reader'], $paths); } /** - * DocumentManager mock object with - * annotation mapping driver - * - * @param string $dbName - * @param MappingDriver $mappingDriver - * - * @return DocumentManager + * DocumentManager mock object together with + * annotation mapping driver and database */ - protected function getMockMappedDocumentManager($dbName, MappingDriver $mappingDriver = null) + protected function getMockDocumentManager(string $dbName, ?MappingDriver $mappingDriver = null): DocumentManager { - $conn = $this->getMock('Doctrine\\MongoDB\\Connection'); - $config = $this->getMockAnnotatedODMMongoDBConfig($dbName, $mappingDriver); + if (!extension_loaded('mongodb')) { + static::markTestSkipped('Missing Mongo extension.'); + } - $dm = DocumentManager::create($conn, $config, $this->getEventManager()); + $client = new Client($_ENV['MONGODB_SERVER'], [], ['typeMap' => DocumentManager::CLIENT_TYPEMAP]); + $config = $this->getMockODMMongoDBConfig($dbName, $mappingDriver); - return $dm; + return DocumentManager::create($client, $config, $this->getEventManager()); } /** @@ -124,84 +117,38 @@ protected function getMockMappedDocumentManager($dbName, MappingDriver $mappingD * annotation mapping driver and pdo_sqlite * database in memory * - * @param array $fixtures - * @param MappingDriver $mappingDriver + * @param string[] $fixtures * - * @return EntityManager + * @phpstan-assert class-string[] $fixtures */ - protected function getMockSqliteEntityManager(array $fixtures, MappingDriver $mappingDriver = null) + protected function getDefaultMockSqliteEntityManager(array $fixtures, ?MappingDriver $mappingDriver = null): EntityManager { - $conn = array( + $conn = [ 'driver' => 'pdo_sqlite', 'memory' => true, - ); + ]; + + $config = $this->getMockORMConfig($mappingDriver); + $connection = DriverManager::getConnection($conn, $config); + $em = new EntityManager($connection, $config, $this->getEventManager()); - $config = $this->getMockAnnotatedORMConfig($mappingDriver); - $em = EntityManager::create($conn, $config, $this->getEventManager()); + $schema = array_map(static function (string $class) use ($em): ClassMetadata { + assert(class_exists($class)); - $schema = array_map(function ($class) use ($em) { return $em->getClassMetadata($class); }, $fixtures); $schemaTool = new SchemaTool($em); - $schemaTool->dropSchema(array()); + $schemaTool->dropSchema([]); $schemaTool->createSchema($schema); return $em; } - /** - * EntityManager mock object with - * annotation mapping driver - * - * @param MappingDriver $mappingDriver - * - * @return EntityManager - */ - protected function getMockMappedEntityManager(MappingDriver $mappingDriver = null) - { - $driver = $this->getMock('Doctrine\DBAL\Driver'); - $driver->expects($this->once()) - ->method('getDatabasePlatform') - ->will($this->returnValue($this->getMock('Doctrine\DBAL\Platforms\MySqlPlatform'))); - - $conn = $this->getMock('Doctrine\DBAL\Connection', array(), array(array(), $driver)); - $conn->expects($this->once()) - ->method('getEventManager') - ->will($this->returnValue($this->getEventManager())); - - $config = $this->getMockAnnotatedORMConfig($mappingDriver); - $em = EntityManager::create($conn, $config); - - return $em; - } - - /** - * Creates default mapping driver - * - * @return MappingDriver - */ - protected function getDefaultORMMetadataDriverImplementation() - { - return new AnnotationDriverORM($_ENV['annotation_reader']); - } - - /** - * Creates default mapping driver - * - * @return MappingDriver - */ - protected function getDefaultMongoODMMetadataDriverImplementation() - { - return new AnnotationDriverODM($_ENV['annotation_reader']); - } - /** * Build event manager - * - * @return EventManager */ - private function getEventManager() + private function getEventManager(): EventManager { if (null === $this->evm) { $this->evm = new EventManager(); @@ -217,137 +164,58 @@ private function getEventManager() /** * Get annotation mapping configuration - * - * @param string $dbName - * @param MappingDriver $mappingDriver - * - * @return \Doctrine\ORM\Configuration */ - private function getMockAnnotatedODMMongoDBConfig($dbName, MappingDriver $mappingDriver = null) + private function getMockODMMongoDBConfig(string $dbName, ?MappingDriver $mappingDriver = null): Configuration { - $config = $this->getMock('Doctrine\\ODM\\MongoDB\\Configuration'); - $config->expects($this->once()) - ->method('getProxyDir') - ->will($this->returnValue(__DIR__.'/../../temp')); - - $config->expects($this->once()) - ->method('getProxyNamespace') - ->will($this->returnValue('Proxy')); - - $config->expects($this->once()) - ->method('getHydratorDir') - ->will($this->returnValue(__DIR__.'/../../temp')); - - $config->expects($this->once()) - ->method('getHydratorNamespace') - ->will($this->returnValue('Hydrator')); - - $config->expects($this->any()) - ->method('getDefaultDB') - ->will($this->returnValue($dbName)); - - $config->expects($this->once()) - ->method('getAutoGenerateProxyClasses') - ->will($this->returnValue(true)); - - $config->expects($this->once()) - ->method('getAutoGenerateHydratorClasses') - ->will($this->returnValue(true)); - - $config->expects($this->once()) - ->method('getClassMetadataFactoryName') - ->will($this->returnValue('Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory')); - - $config - ->expects($this->any()) - ->method('getMongoCmd') - ->will($this->returnValue('$')) - ; - - $config - ->expects($this->any()) - ->method('getDefaultCommitOptions') - ->will($this->returnValue(array('safe' => true))) - ; - if (null === $mappingDriver) { - $mappingDriver = $this->getDefaultMongoODMMetadataDriverImplementation(); + $mappingDriver = $this->getMongoDBDriver(); } - $config->expects($this->any()) - ->method('getMetadataDriverImpl') - ->will($this->returnValue($mappingDriver)); + $config = new Configuration(); - $config->expects($this->any()) - ->method('getRepositoryFactory') - ->will($this->returnValue(new DefaultRepositoryFactoryODM())); + /** @phpstan-ignore-next-line function.alreadyNarrowedType */ + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'setUseNativeLazyObject')) { + $config->setUseNativeLazyObject(true); + } - $config->expects($this->any()) - ->method('getDefaultRepositoryClassName') - ->will($this->returnValue('Doctrine\\ODM\\MongoDB\\DocumentRepository')); + $config->addFilter('softdeleteable', SoftDeleteableFilter::class); + $config->setProxyDir(TESTS_TEMP_DIR); + $config->setHydratorDir(TESTS_TEMP_DIR); + $config->setProxyNamespace('Proxy'); + $config->setHydratorNamespace('Hydrator'); + $config->setDefaultDB('gedmo_extensions_test'); + $config->setAutoGenerateProxyClasses(Configuration::AUTOGENERATE_EVAL); + $config->setAutoGenerateHydratorClasses(Configuration::AUTOGENERATE_EVAL); + $config->setMetadataDriverImpl($mappingDriver); + $config->setMetadataCache(new ArrayAdapter()); return $config; } /** * Get annotation mapping configuration for ORM - * - * @param MappingDriver $mappingDriver - * - * @return \Doctrine\ORM\Configuration */ - private function getMockAnnotatedORMConfig(MappingDriver $mappingDriver = null) + private function getMockORMConfig(?MappingDriver $mappingDriver = null): \Doctrine\ORM\Configuration { - $config = $this->getMock('Doctrine\ORM\Configuration'); - $config->expects($this->once()) - ->method('getProxyDir') - ->will($this->returnValue(__DIR__.'/../../temp')); - - $config->expects($this->once()) - ->method('getProxyNamespace') - ->will($this->returnValue('Proxy')); - - $config->expects($this->any()) - ->method('getDefaultQueryHints') - ->will($this->returnValue(array())); - - $config->expects($this->once()) - ->method('getAutoGenerateProxyClasses') - ->will($this->returnValue(true)); - - $config->expects($this->once()) - ->method('getClassMetadataFactoryName') - ->will($this->returnValue('Doctrine\\ORM\\Mapping\\ClassMetadataFactory')); - - $config - ->expects($this->any()) - ->method('getDefaultRepositoryClassName') - ->will($this->returnValue('Doctrine\\ORM\\EntityRepository')) - ; - - $config - ->expects($this->any()) - ->method('getQuoteStrategy') - ->will($this->returnValue(new DefaultQuoteStrategy())) - ; - - $config - ->expects($this->any()) - ->method('getNamingStrategy') - ->will($this->returnValue(new DefaultNamingStrategy())) - ; - if (null === $mappingDriver) { - $mappingDriver = $this->getDefaultORMMetadataDriverImplementation(); + $config = new \Doctrine\ORM\Configuration(); + + /** @phpstan-ignore-next-line function.alreadyNarrowedType */ + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'enableNativeLazyObjects')) { + $config->enableNativeLazyObjects(true); + } else { + $config->setProxyDir(TESTS_TEMP_DIR); + $config->setProxyNamespace('Proxy'); + $config->setAutoGenerateProxyClasses(true); } - $config->expects($this->any()) - ->method('getMetadataDriverImpl') - ->will($this->returnValue($mappingDriver)); - - $config - ->expects($this->once()) - ->method('getRepositoryFactory') - ->will($this->returnValue(new DefaultRepositoryFactoryORM())); + $config->setDefaultQueryHints([]); + $config->setClassMetadataFactoryName(ClassMetadataFactory::class); + $config->setDefaultRepositoryClassName(EntityRepository::class); + $config->setQuoteStrategy(new DefaultQuoteStrategy()); + $config->setNamingStrategy(new DefaultNamingStrategy()); + $config->setMetadataDriverImpl($mappingDriver ?? $this->getORMDriver()); + $config->setRepositoryFactory(new DefaultRepositoryFactoryORM()); + $config->setMetadataCache(new ArrayAdapter()); return $config; } diff --git a/tests/Gedmo/Tool/BaseTestCaseORM.php b/tests/Gedmo/Tool/BaseTestCaseORM.php index c6c8ab4b8c..c85341b97b 100644 --- a/tests/Gedmo/Tool/BaseTestCaseORM.php +++ b/tests/Gedmo/Tool/BaseTestCaseORM.php @@ -1,22 +1,33 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tool; -use Gedmo\Tool\Logging\DBAL\QueryAnalyzer; -use Doctrine\ORM\Mapping\Driver\AnnotationDriver; -use Doctrine\ORM\EntityManager; use Doctrine\Common\EventManager; -use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Logging\Middleware; use Doctrine\ORM\Configuration; -use Gedmo\Translatable\TranslatableListener; -use Gedmo\Sluggable\SluggableListener; -use Gedmo\Tree\TreeListener; -use Gedmo\Timestampable\TimestampableListener; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Gedmo\Loggable\LoggableListener; +use Gedmo\Sluggable\SluggableListener; use Gedmo\SoftDeleteable\SoftDeleteableListener; -use Doctrine\ORM\Mapping\DefaultQuoteStrategy; -use Doctrine\ORM\Mapping\DefaultNamingStrategy; -use Doctrine\ORM\Repository\DefaultRepositoryFactory; +use Gedmo\Timestampable\TimestampableListener; +use Gedmo\Translatable\TranslatableListener; +use Gedmo\Tree\TreeListener; +use PHPUnit\Framework\TestCase; /** * Base test case contains common mock objects @@ -24,178 +35,88 @@ * ORM object manager * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -abstract class BaseTestCaseORM extends \PHPUnit_Framework_TestCase +abstract class BaseTestCaseORM extends TestCase { - /** - * @var EntityManager - */ - protected $em; + protected ?EntityManager $em = null; - /** - * @var QueryAnalyzer - */ - protected $queryAnalyzer; + protected QueryLogger $queryLogger; - /** - * {@inheritdoc} - */ - protected function setUp() + protected function setUp(): void { + $this->queryLogger = new QueryLogger(); } /** * EntityManager mock object together with * annotation mapping driver and pdo_sqlite * database in memory - * - * @param EventManager $evm - * - * @return EntityManager */ - protected function getMockSqliteEntityManager(EventManager $evm = null, Configuration $config = null) + protected function getDefaultMockSqliteEntityManager(?EventManager $evm = null, ?Configuration $config = null): EntityManager { - $conn = array( + $conn = [ 'driver' => 'pdo_sqlite', 'memory' => true, - ); + ]; - $config = null === $config ? $this->getMockAnnotatedConfig() : $config; - $em = EntityManager::create($conn, $config, $evm ?: $this->getEventManager()); + $config ??= $this->getDefaultConfiguration(); + $connection = DriverManager::getConnection($conn, $config); + $em = new EntityManager($connection, $config, $evm ?? $this->getEventManager()); - $schema = array_map(function ($class) use ($em) { - return $em->getClassMetadata($class); - }, (array) $this->getUsedEntityFixtures()); + $schema = array_map(static fn (string $class): ClassMetadata => $em->getClassMetadata($class), $this->getUsedEntityFixtures()); $schemaTool = new SchemaTool($em); - $schemaTool->dropSchema(array()); + $schemaTool->dropSchema([]); $schemaTool->createSchema($schema); return $this->em = $em; } /** - * EntityManager mock object together with - * annotation mapping driver and custom - * connection - * - * @param array $conn - * @param EventManager $evm - * - * @return EntityManager + * Creates default mapping driver */ - protected function getMockCustomEntityManager(array $conn, EventManager $evm = null) + protected function getMetadataDriverImplementation(): MappingDriver { - $config = $this->getMockAnnotatedConfig(); - $em = EntityManager::create($conn, $config, $evm ?: $this->getEventManager()); - - $schema = array_map(function ($class) use ($em) { - return $em->getClassMetadata($class); - }, (array) $this->getUsedEntityFixtures()); - - $schemaTool = new SchemaTool($em); - $schemaTool->dropSchema(array()); - $schemaTool->createSchema($schema); + if (PHP_VERSION_ID >= 80000) { + return new AttributeDriver([]); + } - return $this->em = $em; + return new AnnotationDriver($_ENV['annotation_reader']); } /** - * EntityManager mock object with - * annotation mapping driver + * Get a list of used fixture classes * - * @param EventManager $evm + * @return array * - * @return EntityManager + * @phpstan-return list */ - protected function getMockMappedEntityManager(EventManager $evm = null) - { - $driver = $this->getMock('Doctrine\DBAL\Driver'); - $driver->expects($this->once()) - ->method('getDatabasePlatform') - ->will($this->returnValue($this->getMock('Doctrine\DBAL\Platforms\MySqlPlatform'))); - - $conn = $this->getMock('Doctrine\DBAL\Connection', array(), array(array(), $driver)); - $conn->expects($this->once()) - ->method('getEventManager') - ->will($this->returnValue($evm ?: $this->getEventManager())); - - $config = $this->getMockAnnotatedConfig(); - $this->em = EntityManager::create($conn, $config); + abstract protected function getUsedEntityFixtures(): array; - return $this->em; - } - - /** - * Starts query statistic log - * - * @throws \RuntimeException - */ - protected function startQueryLog() + protected function getDefaultConfiguration(): Configuration { - if (!$this->em || !$this->em->getConnection()->getDatabasePlatform()) { - throw new \RuntimeException('EntityManager and database platform must be initialized'); + $config = new Configuration(); + + /** @phpstan-ignore-next-line function.alreadyNarrowedType */ + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'enableNativeLazyObjects')) { + $config->enableNativeLazyObjects(true); + } else { + $config->setProxyDir(TESTS_TEMP_DIR); + $config->setProxyNamespace('Gedmo\Mapping\Proxy'); } - $this->queryAnalyzer = new QueryAnalyzer($this->em->getConnection()->getDatabasePlatform()); - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getSQLLogger') - ->will($this->returnValue($this->queryAnalyzer)); - } - /** - * Stops query statistic log and outputs - * the data to screen or file - * - * @param boolean $dumpOnlySql - * @param boolean $writeToLog - * - * @throws \RuntimeException - */ - protected function stopQueryLog($dumpOnlySql = false, $writeToLog = false) - { - if ($this->queryAnalyzer) { - $output = $this->queryAnalyzer->getOutput($dumpOnlySql); - if ($writeToLog) { - $fileName = __DIR__.'/../../temp/query_debug_'.time().'.log'; - if (($file = fopen($fileName, 'w+')) !== false) { - fwrite($file, $output); - fclose($file); - } else { - throw new \RuntimeException('Unable to write to the log file'); - } - } else { - echo $output; - } - } - } + $config->setMetadataDriverImpl($this->getMetadataDriverImplementation()); + $config->setMiddlewares([ + new Middleware($this->queryLogger), + ]); - /** - * Creates default mapping driver - * - * @return \Doctrine\ORM\Mapping\Driver\Driver - */ - protected function getMetadataDriverImplementation() - { - return new AnnotationDriver($_ENV['annotation_reader']); + return $config; } - /** - * Get a list of used fixture classes - * - * @return array - */ - abstract protected function getUsedEntityFixtures(); - /** * Build event manager - * - * @return EventManager */ - private function getEventManager() + private function getEventManager(): EventManager { $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); @@ -207,92 +128,4 @@ private function getEventManager() return $evm; } - - /** - * Get annotation mapping configuration - * - * @return \Doctrine\ORM\Configuration - */ - protected function getMockAnnotatedConfig() - { - // We need to mock every method except the ones which - // handle the filters - $configurationClass = 'Doctrine\ORM\Configuration'; - $refl = new \ReflectionClass($configurationClass); - $methods = $refl->getMethods(); - - $mockMethods = array(); - - foreach ($methods as $method) { - if ($method->name !== 'addFilter' && $method->name !== 'getFilterClassName') { - $mockMethods[] = $method->name; - } - } - - $config = $this->getMock($configurationClass, $mockMethods); - - $config - ->expects($this->once()) - ->method('getProxyDir') - ->will($this->returnValue(__DIR__.'/../../temp')) - ; - - $config - ->expects($this->once()) - ->method('getProxyNamespace') - ->will($this->returnValue('Proxy')) - ; - - $config - ->expects($this->any()) - ->method('getDefaultQueryHints') - ->will($this->returnValue(array())) - ; - - $config - ->expects($this->once()) - ->method('getAutoGenerateProxyClasses') - ->will($this->returnValue(true)) - ; - - $config - ->expects($this->once()) - ->method('getClassMetadataFactoryName') - ->will($this->returnValue('Doctrine\\ORM\\Mapping\\ClassMetadataFactory')) - ; - - $mappingDriver = $this->getMetadataDriverImplementation(); - - $config - ->expects($this->any()) - ->method('getMetadataDriverImpl') - ->will($this->returnValue($mappingDriver)) - ; - - $config - ->expects($this->any()) - ->method('getDefaultRepositoryClassName') - ->will($this->returnValue('Doctrine\\ORM\\EntityRepository')) - ; - - $config - ->expects($this->any()) - ->method('getQuoteStrategy') - ->will($this->returnValue(new DefaultQuoteStrategy())) - ; - - $config - ->expects($this->any()) - ->method('getNamingStrategy') - ->will($this->returnValue(new DefaultNamingStrategy())) - ; - - $config - ->expects($this->once()) - ->method('getRepositoryFactory') - ->will($this->returnValue(new DefaultRepositoryFactory())) - ; - - return $config; - } } diff --git a/tests/Gedmo/Tool/QueryLogger.php b/tests/Gedmo/Tool/QueryLogger.php new file mode 100644 index 0000000000..760a2b14db --- /dev/null +++ b/tests/Gedmo/Tool/QueryLogger.php @@ -0,0 +1,38 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tool; + +use Psr\Log\AbstractLogger; + +final class QueryLogger extends AbstractLogger +{ + /** @var array */ + public array $queries = []; + + /** + * @param mixed $level + * @param string $message + * @param mixed[] $context + */ + public function log($level, $message, array $context = []): void + { + $this->queries[] = [ + 'message' => $message, + 'context' => $context, + ]; + } + + public function reset(): void + { + $this->queries = []; + } +} diff --git a/tests/Gedmo/Translatable/AttributeEntityTranslationTableTest.php b/tests/Gedmo/Translatable/AttributeEntityTranslationTableTest.php new file mode 100644 index 0000000000..63e62122ee --- /dev/null +++ b/tests/Gedmo/Translatable/AttributeEntityTranslationTableTest.php @@ -0,0 +1,115 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Attribute\File; +use Gedmo\Tests\Translatable\Fixture\Attribute\Person; +use Gedmo\Tests\Translatable\Fixture\Attribute\PersonTranslation; +use Gedmo\Translatable\Entity\Repository\TranslationRepository; +use Gedmo\Translatable\TranslatableListener; + +/** + * These are tests for translatable behavior + * + * @author Gediminas Morkevicius + * + * @requires PHP >= 8.0 + */ +final class AttributeEntityTranslationTableTest extends BaseTestCaseORM +{ + private TranslatableListener $translatableListener; + + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $this->translatableListener = new TranslatableListener(); + $this->translatableListener->setTranslatableLocale('en_us'); + $this->translatableListener->setDefaultLocale('en_us'); + $evm->addEventSubscriber($this->translatableListener); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testFixtureGeneratedTranslations(): void + { + $person = new Person(); + $person->setName('name in en'); + + $this->em->persist($person); + $this->em->flush(); + $this->em->clear(); + + $repo = $this->em->getRepository(PersonTranslation::class); + static::assertInstanceOf(TranslationRepository::class, $repo); + + $translations = $repo->findTranslations($person); + // As Translate locale and Default locale are the same, no records should be present in translations table + static::assertCount(0, $translations); + + // test second translations + $person = $this->em->find(Person::class, $person->getId()); + $this->translatableListener->setTranslatableLocale('de_de'); + $person->setName('name in de'); + + $this->em->persist($person); + $this->em->flush(); + $this->em->clear(); + + $translations = $repo->findTranslations($person); + // Only one translation should be present + static::assertCount(1, $translations); + static::assertArrayHasKey('de_de', $translations); + + static::assertArrayHasKey('name', $translations['de_de']); + static::assertSame('name in de', $translations['de_de']['name']); + + $this->translatableListener->setTranslatableLocale('en_us'); + } + + public function testFixtureWithAttributeMappingAndAnnotations(): void + { + $file = new File(); + $file->setTitle('title in en'); + + $this->em->persist($file); + $this->em->flush(); + $this->em->clear(); + + $file = $this->em->find(File::class, $file->getId()); + + $file->locale = 'de'; + $file->setTitle('title in de'); + + $this->em->flush(); + $this->em->clear(); + + $file = $this->em->find(File::class, $file->getId()); + + static::assertSame('title in en', $file->getTitle()); + $file->locale = 'de'; + $this->em->refresh($file); + static::assertSame('title in de', $file->getTitle()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Person::class, + PersonTranslation::class, + File::class, + ]; + } +} diff --git a/tests/Gedmo/Translatable/EntityTranslationTableTest.php b/tests/Gedmo/Translatable/EntityTranslationTableTest.php index 948230a517..204faf6ae2 100644 --- a/tests/Gedmo/Translatable/EntityTranslationTableTest.php +++ b/tests/Gedmo/Translatable/EntityTranslationTableTest.php @@ -1,27 +1,33 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\PersonTranslation; -use Translatable\Fixture\Person; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Person; +use Gedmo\Tests\Translatable\Fixture\PersonTranslation; +use Gedmo\Translatable\Entity\Repository\TranslationRepository; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class EntityTranslationTableTest extends BaseTestCaseORM +final class EntityTranslationTableTest extends BaseTestCaseORM { - const PERSON = 'Translatable\\Fixture\\Person'; - const TRANSLATION = 'Translatable\\Fixture\\PersonTranslation'; - - private $translatableListener; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -31,10 +37,10 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en_us'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testFixtureGeneratedTranslations() + public function testFixtureGeneratedTranslations(): void { $person = new Person(); $person->setName('name in en'); @@ -43,15 +49,15 @@ public function testFixtureGeneratedTranslations() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::TRANSLATION); - $this->assertTrue($repo instanceof Entity\Repository\TranslationRepository); + $repo = $this->em->getRepository(PersonTranslation::class); + static::assertInstanceOf(TranslationRepository::class, $repo); $translations = $repo->findTranslations($person); - //As Translate locale and Default locale are the same, no records should be present in translations table - $this->assertCount(0, $translations); + // As Translate locale and Default locale are the same, no records should be present in translations table + static::assertCount(0, $translations); // test second translations - $person = $this->em->find(self::PERSON, $person->getId()); + $person = $this->em->find(Person::class, $person->getId()); $this->translatableListener->setTranslatableLocale('de_de'); $person->setName('name in de'); @@ -60,28 +66,27 @@ public function testFixtureGeneratedTranslations() $this->em->clear(); $translations = $repo->findTranslations($person); - //Only one translation should be present - $this->assertCount(1, $translations); - $this->assertArrayHasKey('de_de', $translations); + // Only one translation should be present + static::assertCount(1, $translations); + static::assertArrayHasKey('de_de', $translations); - $this->assertArrayHasKey('name', $translations['de_de']); - $this->assertEquals('name in de', $translations['de_de']['name']); + static::assertArrayHasKey('name', $translations['de_de']); + static::assertSame('name in de', $translations['de_de']['name']); $this->translatableListener->setTranslatableLocale('en_us'); } /** * Covers issue #438 - * @test */ - public function shouldPersistDefaultLocaleValue() + public function testShouldPersistDefaultLocaleValue(): void { $this->translatableListener->setPersistDefaultLocaleTranslation(true); $this->translatableListener->setTranslatableLocale('de'); $person = new Person(); $person->setName('de'); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(PersonTranslation::class); $repo ->translate($person, 'name', 'de', 'de') ->translate($person, 'name', 'en_us', 'en_us') @@ -90,20 +95,20 @@ public function shouldPersistDefaultLocaleValue() $this->em->flush(); $this->translatableListener->setTranslatableLocale('en_us'); - $articles = $this->em->createQuery('SELECT p FROM '.self::PERSON.' p')->getArrayResult(); - $this->assertEquals('en_us', $articles[0]['name']); - $trans = $this->em->createQuery('SELECT t FROM '.self::TRANSLATION.' t')->getArrayResult(); - $this->assertCount(2, $trans); + $articles = $this->em->createQuery('SELECT p FROM '.Person::class.' p')->getArrayResult(); + static::assertSame('en_us', $articles[0]['name']); + $trans = $this->em->createQuery('SELECT t FROM '.PersonTranslation::class.' t')->getArrayResult(); + static::assertCount(2, $trans); foreach ($trans as $item) { - $this->assertEquals($item['locale'], $item['content']); + static::assertSame($item['locale'], $item['content']); } } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::PERSON, - self::TRANSLATION, - ); + return [ + Person::class, + PersonTranslation::class, + ]; } } diff --git a/tests/Gedmo/Translatable/Fixture/Article.php b/tests/Gedmo/Translatable/Fixture/Article.php index e5b0851278..0b766544bb 100644 --- a/tests/Gedmo/Translatable/Fixture/Article.php +++ b/tests/Gedmo/Translatable/Fixture/Article.php @@ -1,111 +1,160 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Translatable\Translatable; /** * @ORM\Entity */ +#[ORM\Entity] class Article implements Translatable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[Gedmo\Translatable] + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** * @Gedmo\Translatable + * * @ORM\Column(name="content", type="text", nullable=true) */ - private $content; + #[Gedmo\Translatable] + #[ORM\Column(name: 'content', type: Types::TEXT, nullable: true)] + private ?string $content = null; /** * @Gedmo\Translatable(fallback=false) + * * @ORM\Column(name="views", type="integer", nullable=true) */ - private $views; + #[Gedmo\Translatable(fallback: false)] + #[ORM\Column(name: 'views', type: Types::INTEGER, nullable: true)] + private ?int $views = null; /** * @Gedmo\Translatable(fallback=true) + * * @ORM\Column(name="author", type="string", nullable=true) */ - private $author; + #[Gedmo\Translatable(fallback: true)] + #[ORM\Column(name: 'author', type: Types::STRING, nullable: true)] + private ?string $author = null; /** + * @var string|null + * * Used locale to override Translation listener`s locale + * * @Gedmo\Locale */ - private $locale; + #[Gedmo\Locale] + private ?string $locale = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Comment", mappedBy="article") */ + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article')] private $comments; - public function getId() + public function __construct() + { + $this->comments = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function addComment(Comment $comment) + public function addComment(Comment $comment): void { $comment->setArticle($this); $this->comments[] = $comment; } - public function getComments() + /** + * @return Collection + */ + public function getComments(): Collection { return $this->comments; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setContent($content) + public function setContent(?string $content): void { $this->content = $content; } - public function getContent() + public function getContent(): ?string { return $this->content; } - public function setTranslatableLocale($locale) + public function setTranslatableLocale(?string $locale): void { $this->locale = $locale; } - public function setViews($views) + public function setViews(?int $views): void { $this->views = $views; } - public function getViews() + public function getViews(): ?int { return $this->views; } - public function setAuthor($author) + public function setAuthor(?string $author): void { $this->author = $author; } - public function getAuthor() + public function getAuthor(): ?string { return $this->author; } diff --git a/tests/Gedmo/Translatable/Fixture/Attribute/File.php b/tests/Gedmo/Translatable/Fixture/Attribute/File.php new file mode 100644 index 0000000000..bcdc506fae --- /dev/null +++ b/tests/Gedmo/Translatable/Fixture/Attribute/File.php @@ -0,0 +1,53 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Attribute; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +#[ORM\Entity] +class File +{ + /** + * @var string|null + */ + #[Gedmo\Locale] + public $locale; + + /** + * @var int|null + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + #[Gedmo\Translatable] + private ?string $title = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } +} diff --git a/tests/Gedmo/Translatable/Fixture/Attribute/Person.php b/tests/Gedmo/Translatable/Fixture/Attribute/Person.php new file mode 100644 index 0000000000..33c599b080 --- /dev/null +++ b/tests/Gedmo/Translatable/Fixture/Attribute/Person.php @@ -0,0 +1,45 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Attribute; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +#[ORM\Entity] +#[Gedmo\TranslationEntity(class: PersonTranslation::class)] +class Person +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; + + #[Gedmo\Translatable] + #[ORM\Column(name: 'name', type: Types::STRING, length: 128)] + private ?string $name = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setName(?string $name): void + { + $this->name = $name; + } + + public function getName(): ?string + { + return $this->name; + } +} diff --git a/tests/Gedmo/Translatable/Fixture/Attribute/PersonTranslation.php b/tests/Gedmo/Translatable/Fixture/Attribute/PersonTranslation.php new file mode 100644 index 0000000000..1920a3ba0c --- /dev/null +++ b/tests/Gedmo/Translatable/Fixture/Attribute/PersonTranslation.php @@ -0,0 +1,24 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Attribute; + +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation; +use Gedmo\Translatable\Entity\Repository\TranslationRepository; + +#[ORM\Entity(repositoryClass: TranslationRepository::class)] +#[ORM\Table(name: 'ext_translations')] +#[ORM\Index(name: 'translations_lookup_idx', columns: ['locale', 'object_Class', 'foreign_key'])] +#[ORM\UniqueConstraint(name: 'lookup_unique_idx', columns: ['locale', 'object_Class', 'foreign_key', 'field'])] +class PersonTranslation extends AbstractTranslation +{ +} diff --git a/tests/Gedmo/Translatable/Fixture/Comment.php b/tests/Gedmo/Translatable/Fixture/Comment.php index 98dc94de38..8f0bcc2869 100644 --- a/tests/Gedmo/Translatable/Fixture/Comment.php +++ b/tests/Gedmo/Translatable/Fixture/Comment.php @@ -1,72 +1,103 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Comment { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="subject", type="string", length=128) */ - private $subject; + #[Gedmo\Translatable] + #[ORM\Column(name: 'subject', type: Types::STRING, length: 128)] + private ?string $subject = null; /** * @Gedmo\Translatable + * * @ORM\Column(name="message", type="text") */ - private $message; + #[Gedmo\Translatable] + #[ORM\Column(name: 'message', type: Types::TEXT)] + private ?string $message = null; /** * @ORM\ManyToOne(targetEntity="Article", inversedBy="comments") */ - private $article; + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'comments')] + private ?Article $article = null; /** + * @var string|null + * * Used locale to override Translation listener`s locale + * * @Gedmo\Language */ - private $locale; + #[Gedmo\Language] + private ?string $locale = null; - public function setArticle($article) + public function setArticle(Article $article): void { $this->article = $article; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setSubject($subject) + public function setSubject(?string $subject): void { $this->subject = $subject; } - public function getSubject() + public function getSubject(): ?string { return $this->subject; } - public function setMessage($message) + public function setMessage(?string $message): void { $this->message = $message; } - public function getMessage() + public function getMessage(): ?string { return $this->message; } - public function setTranslatableLocale($locale) + public function setTranslatableLocale(?string $locale): void { $this->locale = $locale; } diff --git a/tests/Gedmo/Translatable/Fixture/Company.php b/tests/Gedmo/Translatable/Fixture/Company.php index c40c1f2ec0..18cc9a62dd 100644 --- a/tests/Gedmo/Translatable/Fixture/Company.php +++ b/tests/Gedmo/Translatable/Fixture/Company.php @@ -1,96 +1,102 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Translatable\Translatable; /** * @ORM\Entity */ +#[ORM\Entity] class Company implements Translatable { - /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=128) + * * @Gedmo\Translatable */ - private $title; + #[Gedmo\Translatable] + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** - * @var CompanyEmbedLink - * @ORM\Embedded(class="Translatable\Fixture\CompanyEmbedLink") + * @ORM\Embedded(class="Gedmo\Tests\Translatable\Fixture\CompanyEmbedLink") */ - private $link; + #[ORM\Embedded(class: CompanyEmbedLink::class)] + private CompanyEmbedLink $link; /** + * @var string|null + * * Used locale to override Translation listener`s locale + * * @Gedmo\Locale */ - private $locale; + #[Gedmo\Locale] + private ?string $locale = null; public function __construct() { $this->link = new CompanyEmbedLink(); } - /** - * @return mixed - */ - public function getId() + public function getId(): ?int { return $this->id; } - /** - * @return mixed - */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } - /** - * @param mixed $title - * @return Company - */ - public function setTitle($title) + public function setTitle(?string $title): self { $this->title = $title; return $this; } - /** - * @return CompanyEmbedLink - */ - public function getLink() + public function getLink(): CompanyEmbedLink { return $this->link; } - /** - * @param mixed $link - * @return Company - */ - public function setLink(CompanyEmbedLink $link) + public function setLink(CompanyEmbedLink $link): self { $this->link = $link; return $this; } - /** - * @param mixed $locale - * @return Company - */ - public function setTranslatableLocale($locale) + public function setTranslatableLocale(?string $locale): self { $this->locale = $locale; return $this; } -} \ No newline at end of file +} diff --git a/tests/Gedmo/Translatable/Fixture/CompanyEmbedLink.php b/tests/Gedmo/Translatable/Fixture/CompanyEmbedLink.php index a71dfc75af..a2d7d74094 100644 --- a/tests/Gedmo/Translatable/Fixture/CompanyEmbedLink.php +++ b/tests/Gedmo/Translatable/Fixture/CompanyEmbedLink.php @@ -1,67 +1,69 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Embeddable */ +#[ORM\Embeddable] class CompanyEmbedLink { /** * @var string * - * @ORM\Column(name="website", type="string", length=255, nullable=true) + * @ORM\Column(name="website", type="string", length=191, nullable=true) + * * @Gedmo\Translatable */ + #[Gedmo\Translatable] + #[ORM\Column(name: 'website', type: Types::STRING, length: 191, nullable: true)] protected $website; /** * @var string * - * @ORM\Column(name="facebook", type="string", length=255, nullable=true) + * @ORM\Column(name="facebook", type="string", length=191, nullable=true) + * * @Gedmo\Translatable */ + #[Gedmo\Translatable] + #[ORM\Column(name: 'facebook', type: Types::STRING, length: 191, nullable: true)] protected $facebook; - /** - * @return string - */ - public function getWebsite() + public function getWebsite(): string { return $this->website; } - /** - * @param string $website - * @return CompanyEmbedLink - */ - public function setWebsite($website) + public function setWebsite(string $website): self { $this->website = $website; return $this; } - /** - * @return string - */ - public function getFacebook() + public function getFacebook(): string { return $this->facebook; } - /** - * @param string $facebook - * @return CompanyEmbedLink - */ - public function setFacebook($facebook) + public function setFacebook(string $facebook): self { $this->facebook = $facebook; return $this; } - -} \ No newline at end of file +} diff --git a/tests/Gedmo/Translatable/Fixture/Document/Article.php b/tests/Gedmo/Translatable/Fixture/Document/Article.php index 70a6e08007..628cfd61a7 100644 --- a/tests/Gedmo/Translatable/Fixture/Document/Article.php +++ b/tests/Gedmo/Translatable/Fixture/Document/Article.php @@ -1,63 +1,91 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** * @MongoODM\Document(collection="articles") */ +#[MongoODM\Document(collection: 'articles')] class Article { - /** @MongoODM\Id */ + /** + * @var string|null + * + * @MongoODM\Id + */ + #[MongoODM\Id] private $id; /** * @Gedmo\Translatable - * @MongoODM\String + * + * @MongoODM\Field(type="string") */ - private $title; + #[Gedmo\Translatable] + #[MongoODM\Field(type: Type::STRING)] + private ?string $title = null; /** * @Gedmo\Translatable - * @MongoODM\String + * + * @MongoODM\Field(type="string") */ - private $code; + #[Gedmo\Translatable] + #[MongoODM\Field(type: Type::STRING)] + private ?string $code = null; /** + * @var string|null + * * @Gedmo\Slug(fields={"title", "code"}) * @Gedmo\Translatable - * @MongoODM\String + * + * @MongoODM\Field(type="string") */ + #[Gedmo\Translatable] + #[MongoODM\Field(type: Type::STRING)] + #[Gedmo\Slug(fields: ['title', 'code'])] private $slug; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } diff --git a/tests/Gedmo/Translatable/Fixture/Document/Personal/Article.php b/tests/Gedmo/Translatable/Fixture/Document/Personal/Article.php index a108cbd1d8..b83f2d4f18 100644 --- a/tests/Gedmo/Translatable/Fixture/Document/Personal/Article.php +++ b/tests/Gedmo/Translatable/Fixture/Document/Personal/Article.php @@ -1,36 +1,77 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Document\Personal; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** - * @Gedmo\TranslationEntity(class="Translatable\Fixture\Document\Personal\ArticleTranslation") + * @Gedmo\TranslationEntity(class="Gedmo\Tests\Translatable\Fixture\Document\Personal\ArticleTranslation") + * * @MongoODM\Document(collection="articles") */ +#[Gedmo\TranslationEntity(class: ArticleTranslation::class)] +#[MongoODM\Document(collection: 'articles')] class Article { - /** @MongoODM\Id */ + /** + * @var string|null + * + * @MongoODM\Id + */ + #[MongoODM\Id] private $id; /** * @Gedmo\Translatable - * @MongoODM\String + * + * @MongoODM\Field(type="string") */ - private $title; + #[Gedmo\Translatable] + #[MongoODM\Field(type: Type::STRING)] + private ?string $title = null; /** - * @MongoODM\ReferenceMany(targetDocument="ArticleTranslation", mappedBy="object") + * @var Collection + * + * @MongoODM\ReferenceMany(targetDocument="Gedmo\Tests\Translatable\Fixture\Document\Personal\ArticleTranslation", mappedBy="object") */ + #[MongoODM\ReferenceMany(targetDocument: ArticleTranslation::class, mappedBy: 'object')] private $translations; - public function getTranslations() + private ?string $code = null; + + /** + * @var string + */ + private $slug; + + public function __construct() + { + $this->translations = new ArrayCollection(); + } + + /** + * @return Collection + */ + public function getTranslations(): Collection { return $this->translations; } - public function addTranslation(PersonalArticleTranslation $t) + public function addTranslation(ArticleTranslation $t): void { if (!$this->translations->contains($t)) { $this->translations[] = $t; @@ -38,32 +79,32 @@ public function addTranslation(PersonalArticleTranslation $t) } } - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCode($code) + public function setCode(?string $code): void { $this->code = $code; } - public function getCode() + public function getCode(): ?string { return $this->code; } - public function getSlug() + public function getSlug(): string { return $this->slug; } diff --git a/tests/Gedmo/Translatable/Fixture/Document/Personal/ArticleTranslation.php b/tests/Gedmo/Translatable/Fixture/Document/Personal/ArticleTranslation.php index 5b1c2cbbc5..cbab95d425 100644 --- a/tests/Gedmo/Translatable/Fixture/Document/Personal/ArticleTranslation.php +++ b/tests/Gedmo/Translatable/Fixture/Document/Personal/ArticleTranslation.php @@ -1,5 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Document\Personal; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; use Gedmo\Translatable\Document\MappedSuperclass\AbstractPersonalTranslation; @@ -7,10 +17,12 @@ /** * @MongoODM\Document(collection="article_translations") */ +#[MongoODM\Document(collection: 'article_translations')] class ArticleTranslation extends AbstractPersonalTranslation { /** - * @MongoODM\ReferenceOne(targetDocument="Article", inversedBy="translations") + * @MongoODM\ReferenceOne(targetDocument="Gedmo\Tests\Translatable\Fixture\Document\Personal\Article", inversedBy="translations") */ + #[MongoODM\ReferenceOne(targetDocument: Article::class, inversedBy: 'translations')] protected $object; } diff --git a/tests/Gedmo/Translatable/Fixture/Document/SimpleArticle.php b/tests/Gedmo/Translatable/Fixture/Document/SimpleArticle.php index 029cf13815..a67d23eade 100644 --- a/tests/Gedmo/Translatable/Fixture/Document/SimpleArticle.php +++ b/tests/Gedmo/Translatable/Fixture/Document/SimpleArticle.php @@ -1,51 +1,73 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** * @MongoODM\Document(collection="articles") */ +#[MongoODM\Document(collection: 'articles')] class SimpleArticle { - /** @MongoODM\Id */ + /** + * @var string|null + * + * @MongoODM\Id + */ + #[MongoODM\Id] private $id; /** * @Gedmo\Translatable - * @MongoODM\String + * + * @MongoODM\Field(type="string") */ - private $title; + #[Gedmo\Translatable] + #[MongoODM\Field(type: Type::STRING)] + private ?string $title = null; /** * @Gedmo\Translatable - * @MongoODM\String + * + * @MongoODM\Field(type="string") */ - private $content; + #[Gedmo\Translatable] + #[MongoODM\Field(type: Type::STRING)] + private ?string $content = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setContent($content) + public function setContent(?string $content): void { $this->content = $content; } - public function getContent() + public function getContent(): ?string { return $this->content; } diff --git a/tests/Gedmo/Translatable/Fixture/File.php b/tests/Gedmo/Translatable/Fixture/File.php index c54d420b83..4e4dc983a4 100644 --- a/tests/Gedmo/Translatable/Fixture/File.php +++ b/tests/Gedmo/Translatable/Fixture/File.php @@ -1,52 +1,80 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="discriminator", type="string") - * @ORM\DiscriminatorMap({"file" = "File", "image" = "Image"}) + * @ORM\DiscriminatorMap({"file": "File", "image": "Image"}) */ +#[ORM\Entity] +#[ORM\InheritanceType('JOINED')] +#[ORM\DiscriminatorColumn(name: 'discriminator', type: Types::STRING)] +#[ORM\DiscriminatorMap(['file' => File::class, 'image' => Image::class])] class File { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(length=128) */ - private $name; + #[Gedmo\Translatable] + #[ORM\Column(length: 128)] + private ?string $name = null; /** * @ORM\Column(type="integer") */ - private $size; + #[ORM\Column(type: Types::INTEGER)] + private ?int $size = null; + + public function getId(): ?int + { + return $this->id; + } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setSize($size) + public function setSize(?int $size): void { $this->size = $size; } - public function getSize() + public function getSize(): ?int { return $this->size; } diff --git a/tests/Gedmo/Translatable/Fixture/Image.php b/tests/Gedmo/Translatable/Fixture/Image.php index c64bb1316e..e90905129e 100644 --- a/tests/Gedmo/Translatable/Fixture/Image.php +++ b/tests/Gedmo/Translatable/Fixture/Image.php @@ -1,27 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; -use Gedmo\Mapping\Annotation as Gedmo; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Image extends File { /** * @Gedmo\Translatable + * * @ORM\Column(length=128) */ - private $mime; + #[Gedmo\Translatable] + #[ORM\Column(length: 128)] + private ?string $mime = null; - public function setMime($mime) + public function setMime(?string $mime): void { $this->mime = $mime; } - public function getMime() + public function getMime(): ?string { return $this->mime; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue1123/BaseEntity.php b/tests/Gedmo/Translatable/Fixture/Issue1123/BaseEntity.php new file mode 100644 index 0000000000..709fa23ca9 --- /dev/null +++ b/tests/Gedmo/Translatable/Fixture/Issue1123/BaseEntity.php @@ -0,0 +1,45 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue1123; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + * @ORM\Table("base_entity") + * @ORM\Inheritancetype("JOINED") + * @ORM\DiscriminatorColumn(name="discr", type="string") + * @ORM\DiscriminatorMap({ + * "base": "BaseEntity", + * "child": "ChildEntity" + * }) + */ +#[ORM\Entity] +#[ORM\Table(name: 'base_entity')] +#[ORM\InheritanceType('JOINED')] +#[ORM\DiscriminatorColumn(name: 'discr', type: Types::STRING)] +#[ORM\DiscriminatorMap(['base' => BaseEntity::class, 'child' => ChildEntity::class])] +abstract class BaseEntity +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + protected $id; +} diff --git a/tests/Gedmo/Translatable/Fixture/Issue1123/ChildEntity.php b/tests/Gedmo/Translatable/Fixture/Issue1123/ChildEntity.php new file mode 100644 index 0000000000..7bf92e055d --- /dev/null +++ b/tests/Gedmo/Translatable/Fixture/Issue1123/ChildEntity.php @@ -0,0 +1,58 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue1123; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Translatable\Translatable; + +/** + * @ORM\Entity + * @ORM\Table("child_entity") + */ +#[ORM\Entity] +#[ORM\Table(name: 'child_entity')] +class ChildEntity extends BaseEntity implements Translatable +{ + /** + * @Gedmo\Translatable + * + * @ORM\Column(name="childTitle", type="string", length=128, nullable=true) + */ + #[ORM\Column(name: 'childTitle', type: Types::STRING, length: 128, nullable: true)] + #[Gedmo\Translatable] + private ?string $childTitle = null; + + /** + * @Gedmo\Locale + * Used locale to override Translation listener`s locale + * this is not a mapped field of entity metadata, just a simple property + */ + #[Gedmo\Locale] + private string $locale = 'en'; + + public function getChildTitle(): ?string + { + return $this->childTitle; + } + + public function setChildTitle(?string $childTitle): void + { + $this->childTitle = $childTitle; + } + + public function setTranslatableLocale(string $locale): void + { + $this->locale = $locale; + } +} diff --git a/tests/Gedmo/Translatable/Fixture/Issue114/Article.php b/tests/Gedmo/Translatable/Fixture/Issue114/Article.php index 9c7a2ca7b9..1955b0ec65 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue114/Article.php +++ b/tests/Gedmo/Translatable/Fixture/Issue114/Article.php @@ -1,54 +1,74 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue114; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + #[Gedmo\Translatable] + private ?string $title = null; /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="articles") */ - private $category; + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'articles')] + private ?Category $category = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCategory(Category $category) + public function setCategory(Category $category): void { $this->category = $category; } - public function getCategory() + public function getCategory(): ?Category { return $this->category; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue114/Category.php b/tests/Gedmo/Translatable/Fixture/Issue114/Category.php index 94107c1477..9e7f1be879 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue114/Category.php +++ b/tests/Gedmo/Translatable/Fixture/Issue114/Category.php @@ -1,54 +1,86 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue114; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Category { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[Gedmo\Translatable] + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="category", cascade={"persist", "remove"}) */ + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category', cascade: ['persist', 'remove'])] private $articles; - public function getId() + public function __construct() + { + $this->articles = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function addArticle(Article $article) + public function addArticle(Article $article): void { $this->articles[] = $article; } - public function getArticles() + /** + * @return Collection + */ + public function getArticles(): Collection { return $this->articles; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue138/Article.php b/tests/Gedmo/Translatable/Fixture/Issue138/Article.php index c61fe8d9c3..3245cbb668 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue138/Article.php +++ b/tests/Gedmo/Translatable/Fixture/Issue138/Article.php @@ -1,55 +1,77 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue138; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(length=128) */ - private $title; + #[Gedmo\Translatable] + #[ORM\Column(length: 128)] + private ?string $title = null; /** * @Gedmo\Translatable + * * @ORM\Column(length=128) */ - private $titleTest; + #[Gedmo\Translatable] + #[ORM\Column(length: 128)] + private ?string $titleTest = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setTitleTest($titleTest) + public function setTitleTest(?string $titleTest): void { $this->titleTest = $titleTest; } - public function getTitleTest() + public function getTitleTest(): ?string { return $this->titleTest; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue165/SimpleArticle.php b/tests/Gedmo/Translatable/Fixture/Issue165/SimpleArticle.php index 179e0e40f2..965427526d 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue165/SimpleArticle.php +++ b/tests/Gedmo/Translatable/Fixture/Issue165/SimpleArticle.php @@ -1,66 +1,89 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue165; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** * @MongoODM\Document(collection="articles") */ +#[MongoODM\Document(collection: 'articles')] class SimpleArticle { - /** @MongoODM\Id */ + /** + * @var string|null + * + * @MongoODM\Id + */ + #[MongoODM\Id] private $id; /** * @Gedmo\Translatable - * @MongoODM\String + * + * @MongoODM\Field(type="string") */ - private $title; + #[Gedmo\Translatable] + #[MongoODM\Field(type: Type::STRING)] + private ?string $title = null; /** * @Gedmo\Translatable - * @MongoODM\String + * + * @MongoODM\Field(type="string") */ - private $content; + #[Gedmo\Translatable] + #[MongoODM\Field(type: Type::STRING)] + private ?string $content = null; /** - * @MongoODM\String + * @MongoODM\Field(type="string") */ - private $untranslated; + #[MongoODM\Field(type: Type::STRING)] + private ?string $untranslated = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setContent($content) + public function setContent(?string $content): void { $this->content = $content; } - public function getContent() + public function getContent(): ?string { return $this->content; } - public function setUntranslated($untranslated) + public function setUntranslated(?string $untranslated): void { $this->untranslated = $untranslated; } - public function getUntranslated() + public function getUntranslated(): ?string { return $this->untranslated; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue173/Article.php b/tests/Gedmo/Translatable/Fixture/Issue173/Article.php index 4cc7223095..d9405d0c36 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue173/Article.php +++ b/tests/Gedmo/Translatable/Fixture/Issue173/Article.php @@ -1,54 +1,74 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue173; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[Gedmo\Translatable] + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="articles") */ - private $category; + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'articles')] + private ?Category $category = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCategory(Category $category) + public function setCategory(Category $category): void { $this->category = $category; } - public function getCategory() + public function getCategory(): ?Category { return $this->category; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue173/Category.php b/tests/Gedmo/Translatable/Fixture/Issue173/Category.php index 4f2a0c5ae6..13497f4be2 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue173/Category.php +++ b/tests/Gedmo/Translatable/Fixture/Issue173/Category.php @@ -1,69 +1,108 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue173; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Category { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + #[Gedmo\Translatable] + private ?string $title = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="category", cascade={"persist", "remove"}) */ + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category', cascade: ['persist', 'remove'])] private $articles; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Product", mappedBy="category", cascade={"persist", "remove"}) */ + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category', cascade: ['persist', 'remove'])] private $products; - public function getId() + public function __construct() + { + $this->articles = new ArrayCollection(); + $this->products = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function addArticles(Article $article) + public function addArticles(Article $article): void { $this->articles[] = $article; } - public function getArticles() + /** + * @return Collection + */ + public function getArticles(): Collection { return $this->articles; } - public function addProducts(Product $product) + public function addProducts(Product $product): void { $this->products[] = $product; } - public function getProducts() + /** + * @return Collection + */ + public function getProducts(): Collection { return $this->products; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue173/Product.php b/tests/Gedmo/Translatable/Fixture/Issue173/Product.php index b32e8c6fd2..53c720539c 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue173/Product.php +++ b/tests/Gedmo/Translatable/Fixture/Issue173/Product.php @@ -1,54 +1,74 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue173; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Product { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + #[Gedmo\Translatable] + private ?string $title = null; /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="products") */ - private $category; + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')] + private ?Category $category = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setCategory(Category $category) + public function setCategory(Category $category): void { $this->category = $category; } - public function getCategory() + public function getCategory(): ?Category { return $this->category; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue2152/EntityWithTranslatableBoolean.php b/tests/Gedmo/Translatable/Fixture/Issue2152/EntityWithTranslatableBoolean.php new file mode 100644 index 0000000000..cc038c4be4 --- /dev/null +++ b/tests/Gedmo/Translatable/Fixture/Issue2152/EntityWithTranslatableBoolean.php @@ -0,0 +1,93 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue2152; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * @ORM\Table("entity") + */ +#[ORM\Entity] +#[ORM\Table(name: 'entity')] +class EntityWithTranslatableBoolean +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * + * @var int + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @Gedmo\Translatable + * + * @ORM\Column(type="string", nullable=true) + */ + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Gedmo\Translatable] + private ?string $title = null; + + /** + * @Gedmo\Translatable + * + * @ORM\Column(type="string", nullable=true) + */ + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Gedmo\Translatable] + private ?string $isOperating = null; + + /** + * @Gedmo\Locale + */ + #[Gedmo\Locale] + private ?string $locale = null; + + public function __construct(string $title, string $isOperating = '0') + { + $this->translateInLocale('en', $title, $isOperating); + } + + public function translateInLocale(string $locale, ?string $title, ?string $isOperating): void + { + $this->title = $title; + $this->isOperating = $isOperating; + $this->locale = $locale; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function isOperating(): ?string + { + return $this->isOperating; + } + + public function getLocale(): ?string + { + return $this->locale; + } +} diff --git a/tests/Gedmo/Translatable/Fixture/Issue2167/Article.php b/tests/Gedmo/Translatable/Fixture/Issue2167/Article.php new file mode 100644 index 0000000000..8c8ae28e96 --- /dev/null +++ b/tests/Gedmo/Translatable/Fixture/Issue2167/Article.php @@ -0,0 +1,75 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue2167; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class Article +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @Gedmo\Translatable + * + * @ORM\Column(name="title", type="string", length=128) + */ + #[Gedmo\Translatable] + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; + + /** + * @Gedmo\Locale + */ + #[Gedmo\Locale] + private ?string $locale = null; + + public function getId(): int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getLocale(): string + { + return $this->locale; + } + + public function setLocale(string $locale): void + { + $this->locale = $locale; + } +} diff --git a/tests/Gedmo/Translatable/Fixture/Issue75/Article.php b/tests/Gedmo/Translatable/Fixture/Issue75/Article.php index b39d2caac9..427648edd7 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue75/Article.php +++ b/tests/Gedmo/Translatable/Fixture/Issue75/Article.php @@ -1,60 +1,94 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue75; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[Gedmo\Translatable] + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var Collection + * * @ORM\ManyToMany(targetEntity="Image", inversedBy="articles") * @ORM\JoinTable(name="article_images", - * joinColumns={@ORM\JoinColumn(name="image_id", referencedColumnName="id")}, - * inverseJoinColumns={@ORM\JoinColumn(name="article_id", referencedColumnName="id")} + * joinColumns={@ORM\JoinColumn(name="image_id", referencedColumnName="id")}, + * inverseJoinColumns={@ORM\JoinColumn(name="article_id", referencedColumnName="id")} * ) */ + #[ORM\ManyToMany(targetEntity: Image::class, inversedBy: 'articles')] + #[ORM\JoinTable(name: 'article_images')] + #[ORM\JoinColumn(name: 'image_id', referencedColumnName: 'id')] + #[ORM\InverseJoinColumn(name: 'article_id', referencedColumnName: 'id')] private $images; /** + * @var Collection + * * @ORM\ManyToMany(targetEntity="File") */ + #[ORM\ManyToMany(targetEntity: File::class)] private $files; public function __construct() { // $images is not an array, its a collection // if you want to do such operations you have to construct it - $this->images = new \Doctrine\Common\Collections\ArrayCollection(); + $this->images = new ArrayCollection(); + $this->files = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } - public function addImage(Image $image) + public function addImage(Image $image): void { $this->images[] = $image; } - public function setImages(array $images) + /** + * @param array $images + */ + public function setImages(array $images): void { foreach ($images as $img) { // first check if it does not contain it allready @@ -66,27 +100,33 @@ public function setImages(array $images) } } - public function getImages() + /** + * @return Collection + */ + public function getImages(): Collection { return $this->images; } - public function addFile(File $file) + public function addFile(File $file): void { $this->files[] = $file; } - public function getFiles() + /** + * @return Collection + */ + public function getFiles(): Collection { return $this->files; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue75/File.php b/tests/Gedmo/Translatable/Fixture/Issue75/File.php index 635df85479..7367544ab9 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue75/File.php +++ b/tests/Gedmo/Translatable/Fixture/Issue75/File.php @@ -1,39 +1,58 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue75; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class File { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + #[Gedmo\Translatable] + private ?string $title = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue75/Image.php b/tests/Gedmo/Translatable/Fixture/Issue75/Image.php index 8a3ab47a7f..1c1d085f55 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue75/Image.php +++ b/tests/Gedmo/Translatable/Fixture/Issue75/Image.php @@ -1,54 +1,86 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue75; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Image { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[Gedmo\Translatable] + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var Collection + * * @ORM\ManyToMany(targetEntity="Article", mappedBy="images") */ + #[ORM\ManyToMany(targetEntity: Article::class, mappedBy: 'images')] private $articles; - public function getId() + public function __construct() + { + $this->articles = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function addArticle(Article $article) + public function addArticle(Article $article): void { $this->articles[] = $article; } - public function getArticles() + /** + * @return Collection + */ + public function getArticles(): Collection { return $this->articles; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Translatable/Fixture/Issue922/Post.php b/tests/Gedmo/Translatable/Fixture/Issue922/Post.php index 44fbde7523..c59a434205 100644 --- a/tests/Gedmo/Translatable/Fixture/Issue922/Post.php +++ b/tests/Gedmo/Translatable/Fixture/Issue922/Post.php @@ -1,95 +1,123 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Issue922; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Post { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(type="datetime", nullable=true) */ - private $publishedAt; + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + #[Gedmo\Translatable] + private ?\DateTime $publishedAt = null; /** * @Gedmo\Translatable + * * @ORM\Column(type="time") */ - private $timestampAt; + #[ORM\Column(type: Types::TIME_MUTABLE)] + #[Gedmo\Translatable] + private ?\DateTime $timestampAt = null; /** * @Gedmo\Translatable + * * @ORM\Column(type="date") */ - private $dateAt; + #[ORM\Column(type: Types::DATE_MUTABLE)] + #[Gedmo\Translatable] + private ?\DateTime $dateAt = null; /** * @Gedmo\Translatable + * * @ORM\Column(type="boolean") */ - private $boolean; + #[ORM\Column(type: Types::BOOLEAN)] + #[Gedmo\Translatable] + private ?bool $boolean = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setPublishedAt($publishedAt) + public function setPublishedAt(?\DateTime $publishedAt): self { $this->publishedAt = $publishedAt; return $this; } - public function getPublishedAt() + public function getPublishedAt(): ?\DateTime { return $this->publishedAt; } - public function setTimestampAt($timestampAt) + public function setTimestampAt(?\DateTime $timestampAt): self { $this->timestampAt = $timestampAt; return $this; } - public function getTimestampAt() + public function getTimestampAt(): ?\DateTime { return $this->timestampAt; } - public function setDateAt($dateAt) + public function setDateAt(?\DateTime $dateAt): self { $this->dateAt = $dateAt; return $this; } - public function getDateAt() + public function getDateAt(): ?\DateTime { return $this->dateAt; } - public function setBoolean($boolean) + public function setBoolean(bool $boolean): self { $this->boolean = $boolean; return $this; } - public function getBoolean() + public function getBoolean(): ?bool { return $this->boolean; } diff --git a/tests/Gedmo/Translatable/Fixture/MixedValue.php b/tests/Gedmo/Translatable/Fixture/MixedValue.php index 049d54e1ec..7c1cf709ff 100644 --- a/tests/Gedmo/Translatable/Fixture/MixedValue.php +++ b/tests/Gedmo/Translatable/Fixture/MixedValue.php @@ -1,54 +1,84 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class MixedValue { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(type="datetime") */ - private $date; + #[Gedmo\Translatable] + #[ORM\Column(type: Types::DATETIME_MUTABLE)] + private ?\DateTime $date = null; /** + * @var mixed + * * @Gedmo\Translatable + * * @ORM\Column(type="custom") */ + #[Gedmo\Translatable] + #[ORM\Column(type: 'custom')] private $cust; - public function getId() + public function getId(): ?int { return $this->id; } - public function setDate($date) + public function setDate(\DateTime $date): void { $this->date = $date; } - public function getDate() + public function getDate(): ?\DateTime { return $this->date; } - public function setCust($cust) + /** + * @param mixed $cust + */ + public function setCust($cust): void { $this->cust = $cust; } + /** + * @return mixed + */ public function getCust() { return $this->cust; diff --git a/tests/Gedmo/Translatable/Fixture/Person.php b/tests/Gedmo/Translatable/Fixture/Person.php index e76867db79..2c336b1add 100644 --- a/tests/Gedmo/Translatable/Fixture/Person.php +++ b/tests/Gedmo/Translatable/Fixture/Person.php @@ -1,40 +1,61 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\TranslationEntity(class="PersonTranslation") */ +#[ORM\Entity] +#[Gedmo\TranslationEntity(class: PersonTranslation::class)] class Person { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="name", type="string", length=128) */ - private $name; + #[Gedmo\Translatable] + #[ORM\Column(name: 'name', type: Types::STRING, length: 128)] + private ?string $name = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } diff --git a/tests/Gedmo/Translatable/Fixture/PersonTranslation.php b/tests/Gedmo/Translatable/Fixture/PersonTranslation.php index f6ad4399a2..cf1fc6d81b 100644 --- a/tests/Gedmo/Translatable/Fixture/PersonTranslation.php +++ b/tests/Gedmo/Translatable/Fixture/PersonTranslation.php @@ -1,22 +1,36 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; use Doctrine\ORM\Mapping as ORM; use Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation; +use Gedmo\Translatable\Entity\Repository\TranslationRepository; /** * @ORM\Table( - * name="ext_translations", - * indexes={@ORM\Index(name="translations_lookup_idx", columns={ - * "locale", "object_class", "foreign_key" - * })}, - * uniqueConstraints={@ORM\UniqueConstraint(name="lookup_unique_idx", columns={ - * "locale", "object_class", "foreign_key", "field" - * })} + * name="ext_translations", + * indexes={@ORM\Index(name="translations_lookup_idx", columns={ + * "locale", "object_class", "foreign_key" + * })}, + * uniqueConstraints={@ORM\UniqueConstraint(name="lookup_unique_idx", columns={ + * "locale", "object_class", "foreign_key", "field" + * })} * ) * @ORM\Entity(repositoryClass="Gedmo\Translatable\Entity\Repository\TranslationRepository") */ +#[ORM\Entity(repositoryClass: TranslationRepository::class)] +#[ORM\Table(name: 'ext_translations')] +#[ORM\Index(name: 'translations_lookup_idx', columns: ['locale', 'object_Class', 'foreign_key'])] +#[ORM\UniqueConstraint(name: 'lookup_unique_idx', columns: ['locale', 'object_Class', 'foreign_key', 'field'])] class PersonTranslation extends AbstractTranslation { } diff --git a/tests/Gedmo/Translatable/Fixture/Personal/Article.php b/tests/Gedmo/Translatable/Fixture/Personal/Article.php index dd51f1a16e..dfd92456be 100644 --- a/tests/Gedmo/Translatable/Fixture/Personal/Article.php +++ b/tests/Gedmo/Translatable/Fixture/Personal/Article.php @@ -1,40 +1,74 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Personal; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** - * @Gedmo\TranslationEntity(class="Translatable\Fixture\Personal\PersonalArticleTranslation") + * @Gedmo\TranslationEntity(class="Gedmo\Tests\Translatable\Fixture\Personal\PersonalArticleTranslation") + * * @ORM\Entity */ +#[ORM\Entity] +#[Gedmo\TranslationEntity(class: PersonalArticleTranslation::class)] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(length: 128)] + #[Gedmo\Translatable] + private ?string $title = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="PersonalArticleTranslation", mappedBy="object") */ + #[ORM\OneToMany(targetEntity: PersonalArticleTranslation::class, mappedBy: 'object')] private $translations; - public function getTranslations() + public function __construct() + { + $this->translations = new ArrayCollection(); + } + + /** + * @return Collection + */ + public function getTranslations(): Collection { return $this->translations; } - public function addTranslation(PersonalArticleTranslation $t) + public function addTranslation(PersonalArticleTranslation $t): void { if (!$this->translations->contains($t)) { $this->translations[] = $t; @@ -42,17 +76,17 @@ public function addTranslation(PersonalArticleTranslation $t) } } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Translatable/Fixture/Personal/PersonalArticleTranslation.php b/tests/Gedmo/Translatable/Fixture/Personal/PersonalArticleTranslation.php index 6c7f49685e..80cfbc438d 100644 --- a/tests/Gedmo/Translatable/Fixture/Personal/PersonalArticleTranslation.php +++ b/tests/Gedmo/Translatable/Fixture/Personal/PersonalArticleTranslation.php @@ -1,5 +1,15 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Personal; use Doctrine\ORM\Mapping as ORM; use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation; @@ -8,11 +18,15 @@ * @ORM\Table(name="article_translations") * @ORM\Entity */ +#[ORM\Table(name: 'article_translations')] +#[ORM\Entity] class PersonalArticleTranslation extends AbstractPersonalTranslation { /** * @ORM\ManyToOne(targetEntity="Article", inversedBy="translations") * @ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE") */ + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'translations')] + #[ORM\JoinColumn(name: 'object_id', referencedColumnName: 'id', onDelete: 'CASCADE')] protected $object; } diff --git a/tests/Gedmo/Translatable/Fixture/Sport.php b/tests/Gedmo/Translatable/Fixture/Sport.php index 4c65696d7d..85e556eaad 100644 --- a/tests/Gedmo/Translatable/Fixture/Sport.php +++ b/tests/Gedmo/Translatable/Fixture/Sport.php @@ -1,54 +1,74 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class Sport { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(length=128) */ - private $title; + #[Gedmo\Translatable] + #[ORM\Column(length: 128)] + private ?string $title = null; /** * @ORM\Column(type="text", nullable=true) */ - private $description; + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $description = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setDescription($description) + public function setDescription(?string $description): void { $this->description = $description; } - public function getDescription() + public function getDescription(): ?string { return $this->description; } diff --git a/tests/Gedmo/Translatable/Fixture/StringIdentifier.php b/tests/Gedmo/Translatable/Fixture/StringIdentifier.php index ab13db54a4..630dc6eeb5 100644 --- a/tests/Gedmo/Translatable/Fixture/StringIdentifier.php +++ b/tests/Gedmo/Translatable/Fixture/StringIdentifier.php @@ -1,54 +1,74 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity */ +#[ORM\Entity] class StringIdentifier { /** * @ORM\Id * @ORM\Column(name="uid", type="string", length=32) */ - private $uid; + #[ORM\Id] + #[ORM\Column(name: 'uid', type: Types::STRING, length: 32)] + private ?string $uid = null; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[Gedmo\Translatable] + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var string|null + * * Used locale to override Translation listener`s locale + * * @Gedmo\Locale */ - private $locale; + #[Gedmo\Locale] + private ?string $locale = null; - public function getUid() + public function getUid(): ?string { return $this->uid; } - public function setUid($uid) + public function setUid(?string $uid): void { $this->uid = $uid; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setTranslatableLocale($locale) + public function setTranslatableLocale(?string $locale): void { $this->locale = $locale; } diff --git a/tests/Gedmo/Translatable/Fixture/Template/ArticleTemplate.php b/tests/Gedmo/Translatable/Fixture/Template/ArticleTemplate.php index c6687bcb4e..00d0a061e3 100644 --- a/tests/Gedmo/Translatable/Fixture/Template/ArticleTemplate.php +++ b/tests/Gedmo/Translatable/Fixture/Template/ArticleTemplate.php @@ -1,54 +1,74 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Template; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\MappedSuperclass */ +#[ORM\MappedSuperclass] class ArticleTemplate { + /** + * @var string|null + * + * Used locale to override Translation listener`s locale + * + * @Gedmo\Locale + */ + #[Gedmo\Locale] + protected $locale; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + #[Gedmo\Translatable] + private ?string $title = null; /** * @Gedmo\Translatable + * * @ORM\Column(name="content", type="text") */ - private $content; - - /** - * Used locale to override Translation listener`s locale - * @Gedmo\Locale - */ - protected $locale; + #[ORM\Column(name: 'content', type: Types::TEXT)] + #[Gedmo\Translatable] + private ?string $content = null; - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setContent($content) + public function setContent(?string $content): void { $this->content = $content; } - public function getContent() + public function getContent(): ?string { return $this->content; } - public function setTranslatableLocale($locale) + public function setTranslatableLocale(?string $locale): void { $this->locale = $locale; } diff --git a/tests/Gedmo/Translatable/Fixture/TemplatedArticle.php b/tests/Gedmo/Translatable/Fixture/TemplatedArticle.php index 6c57f5c6d1..2d791058fb 100644 --- a/tests/Gedmo/Translatable/Fixture/TemplatedArticle.php +++ b/tests/Gedmo/Translatable/Fixture/TemplatedArticle.php @@ -1,35 +1,54 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tests\Translatable\Fixture\Template\ArticleTemplate; /** * @ORM\Entity */ +#[ORM\Entity] class TemplatedArticle extends ArticleTemplate { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(type="string", length=128) */ - private $name; + #[Gedmo\Translatable] + #[ORM\Column(type: Types::STRING, length: 128)] + private ?string $name = null; - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } diff --git a/tests/Gedmo/Translatable/Fixture/Type/Custom.php b/tests/Gedmo/Translatable/Fixture/Type/Custom.php index 923fc5227f..246dd5087f 100644 --- a/tests/Gedmo/Translatable/Fixture/Type/Custom.php +++ b/tests/Gedmo/Translatable/Fixture/Type/Custom.php @@ -1,41 +1,170 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Fixture\Type; -use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ArrayType; +use Doctrine\DBAL\Types\Type; + +if (class_exists(ArrayType::class)) { + // DBAL 3.x + /** + * Helper class to address compatibility issues between DBAL 3.x and 4.x. + * + * @internal + */ + abstract class CompatType extends Type + { + /** + * @param array $column + * + * @return string + */ + public function getSQLDeclaration(array $column, AbstractPlatform $platform) + { + return $this->doGetSQLDeclaration($column, $platform); + } + + /** + * @param mixed $value + * + * @return mixed + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return $this->doConvertToDatabaseValue($value, $platform); + } + + /** + * @param mixed $value + * + * @return mixed + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return $this->doConvertToPHPValue($value, $platform); + } + + /** + * @param array $column + */ + abstract protected function doGetSQLDeclaration(array $column, AbstractPlatform $platform): string; + + /** + * @param mixed $value + * + * @return mixed + */ + abstract protected function doConvertToDatabaseValue($value, AbstractPlatform $platform); + + /** + * @param mixed $value + * + * @return mixed + */ + abstract protected function doConvertToPHPValue($value, AbstractPlatform $platform); + } +} else { + // DBAL 4.x + /** + * Helper class to address compatibility issues between DBAL 3.x and 4.x. + * + * @internal + */ + abstract class CompatType extends Type + { + /** + * @param array $column + */ + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string + { + return $this->doGetSQLDeclaration($column, $platform); + } -class Custom extends Type + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed + { + return $this->doConvertToDatabaseValue($value, $platform); + } + + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed + { + return $this->doConvertToPHPValue($value, $platform); + } + + /** + * @param array $column + */ + abstract protected function doGetSQLDeclaration(array $column, AbstractPlatform $platform): string; + + /** + * @param mixed $value + * + * @return mixed + */ + abstract protected function doConvertToDatabaseValue($value, AbstractPlatform $platform); + + /** + * @param mixed $value + * + * @return mixed + */ + abstract protected function doConvertToPHPValue($value, AbstractPlatform $platform); + } +} + +class Custom extends CompatType { - const NAME = 'custom'; + private const NAME = 'custom'; - public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + public function getName(): string { - return $platform->getClobTypeDeclarationSQL($fieldDeclaration); + return self::NAME; + } + + /** + * @param array $column + */ + protected function doGetSQLDeclaration(array $column, AbstractPlatform $platform): string + { + return $platform->getClobTypeDeclarationSQL($column); } - public function convertToDatabaseValue($value, AbstractPlatform $platform) + /** + * @param mixed $value + * + * @return mixed + */ + protected function doConvertToDatabaseValue($value, AbstractPlatform $platform) { return serialize($value); } - public function convertToPHPValue($value, AbstractPlatform $platform) + /** + * @param mixed $value + * + * @return mixed + */ + protected function doConvertToPHPValue($value, AbstractPlatform $platform) { - if ($value === null) { + if (null === $value) { return null; } $value = (is_resource($value)) ? stream_get_contents($value) : $value; $val = unserialize($value); - if ($val === false && $value !== 'b:0;') { - new \Exception('Conversion failed'); + if (false === $val && 'b:0;' !== $value) { + throw new \Exception('Conversion failed'); } return $val; } - - public function getName() - { - return self::NAME; - } } diff --git a/tests/Gedmo/Translatable/InheritanceTest.php b/tests/Gedmo/Translatable/InheritanceTest.php index d6da3bd9ad..520f16ac50 100644 --- a/tests/Gedmo/Translatable/InheritanceTest.php +++ b/tests/Gedmo/Translatable/InheritanceTest.php @@ -1,34 +1,37 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; use Doctrine\ORM\Query; -use Translatable\Fixture\File; -use Translatable\Fixture\Image; -use Translatable\Fixture\TemplatedArticle; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\File; +use Gedmo\Tests\Translatable\Fixture\Image; +use Gedmo\Tests\Translatable\Fixture\TemplatedArticle; +use Gedmo\Translatable\Entity\Repository\TranslationRepository; +use Gedmo\Translatable\Entity\Translation; use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class InheritanceTest extends BaseTestCaseORM +final class InheritanceTest extends BaseTestCaseORM { - const ARTICLE = 'Translatable\\Fixture\\TemplatedArticle'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; - const FILE = 'Translatable\\Fixture\\File'; - const IMAGE = 'Translatable\\Fixture\\Image'; - - const TREE_WALKER_TRANSLATION = 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'; + private TranslatableListener $translatableListener; - private $translatableListener; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -38,13 +41,10 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldHandleMappedSuperclass() + public function testShouldHandleMappedSuperclass(): void { $article = new TemplatedArticle(); $article->setName('name in en'); @@ -55,14 +55,14 @@ public function shouldHandleMappedSuperclass() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::TRANSLATION); - $this->assertTrue($repo instanceof Entity\Repository\TranslationRepository); + $repo = $this->em->getRepository(Translation::class); + static::assertInstanceOf(TranslationRepository::class, $repo); $translations = $repo->findTranslations($article); - $this->assertCount(0, $translations); + static::assertCount(0, $translations); // test second translations - $article = $this->em->getRepository(self::ARTICLE)->find(1); + $article = $this->em->getRepository(TemplatedArticle::class)->find(1); $this->translatableListener->setTranslatableLocale('de'); $article->setName('name in de'); $article->setContent('content in de'); @@ -73,23 +73,20 @@ public function shouldHandleMappedSuperclass() $this->em->clear(); $translations = $repo->findTranslations($article); - $this->assertCount(1, $translations); - $this->assertArrayHasKey('de', $translations); + static::assertCount(1, $translations); + static::assertArrayHasKey('de', $translations); - $this->assertArrayHasKey('name', $translations['de']); - $this->assertEquals('name in de', $translations['de']['name']); + static::assertArrayHasKey('name', $translations['de']); + static::assertSame('name in de', $translations['de']['name']); - $this->assertArrayHasKey('title', $translations['de']); - $this->assertEquals('title in de', $translations['de']['title']); + static::assertArrayHasKey('title', $translations['de']); + static::assertSame('title in de', $translations['de']['title']); - $this->assertArrayHasKey('content', $translations['de']); - $this->assertEquals('content in de', $translations['de']['content']); + static::assertArrayHasKey('content', $translations['de']); + static::assertSame('content in de', $translations['de']['content']); } - /** - * @test - */ - public function shouldHandleInheritedTranslationsThroughBaseObjectClass() + public function testShouldHandleInheritedTranslationsThroughBaseObjectClass(): void { $file = new File(); $file->setSize(500); @@ -113,31 +110,36 @@ public function shouldHandleInheritedTranslationsThroughBaseObjectClass() $this->em->persist($file); $this->em->persist($image); $this->em->flush(); + + $fileId = $file->getId(); + $imageId = $image->getId(); + $this->em->clear(); - $dql = 'SELECT f FROM '.self::FILE.' f'; + $dql = 'SELECT f FROM '.File::class.' f INDEX BY f.id'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $files = $q->getArrayResult(); - $this->assertCount(2, $files); - $this->assertEquals('image de', $files[0]['name']); - $this->assertEquals('file de', $files[1]['name']); + static::assertCount(2, $files); + + static::assertSame('image de', $files[$imageId]['name']); + static::assertSame('file de', $files[$fileId]['name']); // test loading in locale - $images = $this->em->getRepository(self::IMAGE)->findAll(); - $this->assertCount(1, $images); - $this->assertEquals('image de', $images[0]->getName()); - $this->assertEquals('mime de', $images[0]->getMime()); + $images = $this->em->getRepository(Image::class)->findAll(); + static::assertCount(1, $images); + static::assertSame('image de', $images[0]->getName()); + static::assertSame('mime de', $images[0]->getMime()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::TRANSLATION, - self::FILE, - self::IMAGE, - ); + return [ + TemplatedArticle::class, + Translation::class, + File::class, + Image::class, + ]; } } diff --git a/tests/Gedmo/Translatable/Issue/Issue109Test.php b/tests/Gedmo/Translatable/Issue/Issue109Test.php index 26f0073de9..453746b7e6 100644 --- a/tests/Gedmo/Translatable/Issue/Issue109Test.php +++ b/tests/Gedmo/Translatable/Issue/Issue109Test.php @@ -1,32 +1,36 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; use Doctrine\ORM\Query; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Article; +use Gedmo\Tests\Translatable\Fixture\Comment; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Hydrator\ORM\ObjectHydrator; use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; -use Translatable\Fixture\Article; -use Translatable\Fixture\Comment; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translation query walker * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue109Test extends BaseTestCaseORM +final class Issue109Test extends BaseTestCaseORM { - const ARTICLE = 'Translatable\\Fixture\\Article'; - const COMMENT = 'Translatable\\Fixture\\Comment'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; - - const TREE_WALKER_TRANSLATION = 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'; + private TranslatableListener $translatableListener; - private $translatableListener; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -36,22 +40,19 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testIssue109() + public function testIssue109(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator')) - ; + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_OBJECT_TRANSLATION, + ObjectHydrator::class + ); $query = $this->em->createQueryBuilder(); $query->select('a') - ->from(self::ARTICLE, 'a') + ->from(Article::class, 'a') ->add('where', $query->expr()->not($query->expr()->eq('a.title', ':title'))) ->setParameter('title', 'NA') ; @@ -60,22 +61,13 @@ public function testIssue109() $this->translatableListener->setDefaultLocale('en'); $this->translatableListener->setTranslationFallback(true); $query = $query->getQuery(); - $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $result = $query->getResult(); - $this->assertEquals(3, count($result)); + static::assertCount(3, $result); } - protected function getUsedEntityFixtures() - { - return array( - self::ARTICLE, - self::TRANSLATION, - self::COMMENT, - ); - } - - public function populate() + public function populate(): void { $text0 = new Article(); $text0->setTitle('text0'); @@ -105,4 +97,13 @@ public function populate() $this->em->persist($text0); $this->em->flush(); } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + Translation::class, + Comment::class, + ]; + } } diff --git a/tests/Gedmo/Translatable/Issue/Issue1123Test.php b/tests/Gedmo/Translatable/Issue/Issue1123Test.php new file mode 100644 index 0000000000..63efce6cb9 --- /dev/null +++ b/tests/Gedmo/Translatable/Issue/Issue1123Test.php @@ -0,0 +1,93 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Issue; + +use Doctrine\Common\EventManager; +use Doctrine\ORM\Query; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Issue1123\BaseEntity; +use Gedmo\Tests\Translatable\Fixture\Issue1123\ChildEntity; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; +use Gedmo\Translatable\TranslatableListener; + +final class Issue1123Test extends BaseTestCaseORM +{ + private TranslatableListener $translatableListener; + + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $this->translatableListener = new TranslatableListener(); + $this->translatableListener->setTranslatableLocale('en'); + $this->translatableListener->setDefaultLocale('en'); + $this->translatableListener->setTranslationFallback(true); + $evm->addEventSubscriber($this->translatableListener); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testShouldFindInheritedClassTranslations(): void + { + $repo = $this->em->getRepository(Translation::class); + + $title = 'Hello World'; + $deTitle = 'Hallo Welt'; + + // Check that the child class can have translations + $childEntity = new ChildEntity(); + $childEntity->setChildTitle($title); + $this->em->persist($childEntity); + $this->em->flush(); + + $childEntity->setTranslatableLocale('de'); + $childEntity->setChildTitle($deTitle); + $this->em->persist($childEntity); + $this->em->flush(); + + // Clear to be sure... + $this->em->clear(); + + // Find using the repository + $translations = $repo->findTranslations($childEntity); + static::assertCount(1, $translations); + static::assertArrayHasKey('de', $translations); + static::assertSame(['childTitle' => $deTitle], $translations['de']); + + // find using QueryBuilder + $qb = $this->em->createQueryBuilder()->select('e')->from(ChildEntity::class, 'e'); + + $query = $qb->getQuery(); + $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); + $query->setHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'de'); + $query->setHint(TranslatableListener::HINT_FALLBACK, 1); + + $res = $query->getArrayResult(); + static::assertArrayHasKey('id', $res[0]); + static::assertArrayHasKey('childTitle', $res[0]); + static::assertArrayHasKey('discr', $res[0]); + static::assertSame(1, $res[0]['id']); + static::assertSame($deTitle, $res[0]['childTitle']); + static::assertSame('child', $res[0]['discr']); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Translation::class, + BaseEntity::class, + ChildEntity::class, + ]; + } +} diff --git a/tests/Gedmo/Translatable/Issue/Issue114Test.php b/tests/Gedmo/Translatable/Issue/Issue114Test.php index 1f3cfa085f..833d03d2c9 100644 --- a/tests/Gedmo/Translatable/Issue/Issue114Test.php +++ b/tests/Gedmo/Translatable/Issue/Issue114Test.php @@ -1,28 +1,33 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\Issue114\Article; -use Translatable\Fixture\Issue114\Category; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Issue114\Article; +use Gedmo\Tests\Translatable\Fixture\Issue114\Category; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue114Test extends BaseTestCaseORM +final class Issue114Test extends BaseTestCaseORM { - const CATEGORY = 'Translatable\\Fixture\\Issue114\\Category'; - const ARTICLE = 'Translatable\\Fixture\\Issue114\\Article'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; + private TranslatableListener $translatableListener; - private $translatableListener; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -32,14 +37,14 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testIssue114() + public function testIssue114(): void { - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); - //Categories + // Categories $category1 = new Category(); $category1->setTitle('en category1'); @@ -50,7 +55,7 @@ public function testIssue114() $this->em->persist($category2); $this->em->flush(); - //Articles + // Articles $article1 = new Article(); $article1->setTitle('en article1'); $article1->setCategory($category1); @@ -97,22 +102,21 @@ public function testIssue114() $this->em->flush(); $trans = $repo->findTranslations($article2); - $this->assertEquals(1, count($trans)); + static::assertCount(1, $trans); $trans = $repo->findTranslations($article3); - $this->assertEquals(1, count($trans)); + static::assertCount(1, $trans); $trans = $repo->findTranslations($article1); - $this->assertEquals(1, count($trans)); + static::assertCount(1, $trans); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CATEGORY, - self::ARTICLE, - self::TRANSLATION, - - ); + return [ + Category::class, + Article::class, + Translation::class, + ]; } } diff --git a/tests/Gedmo/Translatable/Issue/Issue135Test.php b/tests/Gedmo/Translatable/Issue/Issue135Test.php index 648e6dfdc5..d12a457da9 100644 --- a/tests/Gedmo/Translatable/Issue/Issue135Test.php +++ b/tests/Gedmo/Translatable/Issue/Issue135Test.php @@ -1,32 +1,35 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; use Doctrine\ORM\Query; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Article; +use Gedmo\Tests\Translatable\Fixture\Comment; +use Gedmo\Translatable\Entity\Translation; use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; -use Translatable\Fixture\Article; -use Translatable\Fixture\Comment; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translation query walker * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue135Test extends BaseTestCaseORM +final class Issue135Test extends BaseTestCaseORM { - const ARTICLE = 'Translatable\\Fixture\\Article'; - const COMMENT = 'Translatable\\Fixture\\Comment'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; + private TranslatableListener $translatableListener; - const TREE_WALKER_TRANSLATION = 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'; - - private $translatableListener; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -36,15 +39,15 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en_us'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testIssue135() + public function testIssue135(): void { $query = $this->em->createQueryBuilder(); $query->select('a') - ->from(self::ARTICLE, 'a') + ->from(Article::class, 'a') ->add('where', $query->expr()->not($query->expr()->eq('a.title', ':title'))) ->setParameter('title', 'NA') ; @@ -52,23 +55,14 @@ public function testIssue135() $this->translatableListener->setTranslatableLocale('en'); $this->translatableListener->setTranslationFallback(true); $query = $query->getQuery(); - $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $count = 0; - str_replace("locale = 'en'", '', $query->getSql(), $count); - $this->assertEquals(0, $count); + str_replace("locale = 'en'", '', $query->getSQL(), $count); + static::assertSame(0, $count); } - protected function getUsedEntityFixtures() - { - return array( - self::ARTICLE, - self::TRANSLATION, - self::COMMENT, - ); - } - - public function populate() + public function populate(): void { $this->translatableListener->setTranslatableLocale('en'); $this->translatableListener->setDefaultLocale('en'); @@ -100,4 +94,13 @@ public function populate() $this->em->persist($text0); $this->em->flush(); } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + Translation::class, + Comment::class, + ]; + } } diff --git a/tests/Gedmo/Translatable/Issue/Issue138Test.php b/tests/Gedmo/Translatable/Issue/Issue138Test.php index 7e46dfe84b..96cd47c08e 100644 --- a/tests/Gedmo/Translatable/Issue/Issue138Test.php +++ b/tests/Gedmo/Translatable/Issue/Issue138Test.php @@ -1,29 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\Issue138\Article; -use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; use Doctrine\ORM\Query; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Issue138\Article; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue138Test extends BaseTestCaseORM +final class Issue138Test extends BaseTestCaseORM { - const ARTICLE = 'Translatable\\Fixture\\Issue138\\Article'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; - const TREE_WALKER_TRANSLATION = 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'; - - private $translatableListener; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -34,38 +39,35 @@ protected function setUp() $this->translatableListener->setTranslationFallback(true); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testIssue138() + public function testIssue138(): void { $this->populate(); - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $dql = 'SELECT a FROM '.Article::class.' a'; $dql .= " WHERE a.title LIKE '%foo%'"; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); - //die($q->getSQL()); + // die($q->getSQL()); $result = $q->getArrayResult(); - $this->assertEquals(1, count($result)); - $this->assertEquals('Food', $result[0]['title']); + static::assertCount(1, $result); + static::assertSame('Food', $result[0]['title']); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::TRANSLATION, - - ); + return [ + Article::class, + Translation::class, + ]; } - private function populate() + private function populate(): void { - $repo = $this->em->getRepository(self::ARTICLE); - $food = new Article(); $food->setTitle('Food'); $food->setTitleTest('about food'); diff --git a/tests/Gedmo/Translatable/Issue/Issue165Test.php b/tests/Gedmo/Translatable/Issue/Issue165Test.php index fce2c3e5a2..b2642e1d31 100644 --- a/tests/Gedmo/Translatable/Issue/Issue165Test.php +++ b/tests/Gedmo/Translatable/Issue/Issue165Test.php @@ -1,27 +1,31 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Issue; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Translatable\Fixture\Issue165\SimpleArticle; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; +use Gedmo\Tests\Translatable\Fixture\Issue165\SimpleArticle; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for Translatable behavior ODM implementation * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue165Test extends BaseTestCaseMongoODM +final class Issue165Test extends BaseTestCaseMongoODM { - const ARTICLE = 'Translatable\Fixture\Issue165\SimpleArticle'; - const TRANSLATION = 'Gedmo\\Translatable\\Document\\Translation'; - - private $translatableListener; - private $articleId; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); @@ -30,13 +34,10 @@ protected function setUp() $this->translatableListener->setTranslatableLocale('en'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); } - /** - * @test - */ - public function shouldPersistUntranslatedFields() + public function testShouldPersistUntranslatedFields(): void { $article = new SimpleArticle(); $article->setTitle('en'); @@ -46,7 +47,7 @@ public function shouldPersistUntranslatedFields() $this->dm->persist($article); $this->dm->flush(); - $this->assertEquals('en', $article->getUntranslated()); + static::assertSame('en', $article->getUntranslated()); $this->translatableListener->setTranslatableLocale('ru'); @@ -57,7 +58,7 @@ public function shouldPersistUntranslatedFields() $this->dm->persist($article); $this->dm->flush(); - $this->assertEquals('ru', $article->getUntranslated()); + static::assertSame('ru', $article->getUntranslated()); $this->translatableListener->setTranslatableLocale('de'); @@ -70,12 +71,12 @@ public function shouldPersistUntranslatedFields() $this->dm->flush(); $this->dm->refresh($article); - $this->assertEquals('de', $newarticle->getUntranslated()); + static::assertSame('de', $newarticle->getUntranslated()); $this->translatableListener->setTranslatableLocale('en'); $id = $newarticle->getId(); - $newarticle = $this->dm->getRepository('Translatable\Fixture\Issue165\SimpleArticle')->find($id); + $newarticle = $this->dm->getRepository(SimpleArticle::class)->find($id); $newarticle->setTitle('en'); $newarticle->setContent('en'); @@ -85,7 +86,7 @@ public function shouldPersistUntranslatedFields() $this->dm->flush(); $this->dm->refresh($newarticle); - $this->assertEquals('en', $newarticle->getUntranslated()); + static::assertSame('en', $newarticle->getUntranslated()); $this->translatableListener->setTranslatableLocale('de'); $newarticle->setTitle('de2'); @@ -96,8 +97,8 @@ public function shouldPersistUntranslatedFields() $this->dm->flush(); $id = $newarticle->getId(); - $newarticle = $this->dm->getRepository('Translatable\Fixture\Issue165\SimpleArticle')->find($id); + $newarticle = $this->dm->getRepository(SimpleArticle::class)->find($id); - $this->assertEquals('de2', $newarticle->getUntranslated()); + static::assertSame('de2', $newarticle->getUntranslated()); } } diff --git a/tests/Gedmo/Translatable/Issue/Issue173Test.php b/tests/Gedmo/Translatable/Issue/Issue173Test.php index f4a168a4d7..907abe0dc0 100644 --- a/tests/Gedmo/Translatable/Issue/Issue173Test.php +++ b/tests/Gedmo/Translatable/Issue/Issue173Test.php @@ -1,32 +1,38 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; +use Doctrine\ORM\Query; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Issue173\Article; +use Gedmo\Tests\Translatable\Fixture\Issue173\Category; +use Gedmo\Tests\Translatable\Fixture\Issue173\Product; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Hydrator\ORM\ObjectHydrator; use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; -use Translatable\Fixture\Issue173\Article; -use Translatable\Fixture\Issue173\Category; -use Translatable\Fixture\Issue173\Product; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @contributor Oscar Balladares liebegrube@gmail.com https://github.com/oscarballadares - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * @author Oscar Balladares liebegrube@gmail.com https://github.com/oscarballadares */ -class Issue173Test extends BaseTestCaseORM +final class Issue173Test extends BaseTestCaseORM { - const CATEGORY = 'Translatable\\Fixture\\Issue173\\Category'; - const ARTICLE = 'Translatable\\Fixture\\Issue173\\Article'; - const PRODUCT = 'Translatable\\Fixture\\Issue173\\Product'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; - - private $translatableListener; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -36,58 +42,68 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testIssue173() + public function testIssue173(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator')) - ; + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_OBJECT_TRANSLATION, + ObjectHydrator::class + ); $categories = $this->getCategoriesThatHasNoAssociations(); - $this->assertEquals(count($categories), 1, '$category3 has no associations'); + static::assertCount(1, $categories, '$category3 has no associations'); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Category::class, + Article::class, + Product::class, + Translation::class, + ]; } - public function getCategoriesThatHasNoAssociations() + /** + * @return array + */ + private function getCategoriesThatHasNoAssociations(): array { $query = $this->em->createQueryBuilder(); $query2 = $this->em->createQueryBuilder(); $query3 = $this->em->createQueryBuilder(); $dql1 = $query2 ->select('c1') - ->from(self::CATEGORY, 'c1') + ->from(Category::class, 'c1') ->join('c1.products', 'p') - ->getDql() + ->getDQL() ; $dql2 = $query3 ->select('c2') - ->from(self::CATEGORY, 'c2') + ->from(Category::class, 'c2') ->join('c2.articles', 'a') - ->getDql() + ->getDQL() ; $query ->select('c') - ->from(self::CATEGORY, 'c') + ->from(Category::class, 'c') ->where($query->expr()->notIn('c.id', $dql1)) ->andWhere($query->expr()->notIn('c.id', $dql2)) - ; + ; return $query->getQuery()->setHint( - \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, - 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker' + Query::HINT_CUSTOM_OUTPUT_WALKER, + TranslationWalker::class )->getResult(); } - private function populate() + private function populate(): void { - //Categories + // Categories $category1 = new Category(); $category1->setTitle('en category1'); @@ -102,12 +118,12 @@ private function populate() $this->em->persist($category3); $this->em->flush(); - //Articles + // Articles $article1 = new Article(); $article1->setTitle('en article1'); $article1->setCategory($category1); - //Products + // Products $product1 = new Product(); $product1->setTitle('en product1'); $product1->setCategory($category2); @@ -134,15 +150,4 @@ private function populate() $this->em->flush(); } - - protected function getUsedEntityFixtures() - { - return array( - self::CATEGORY, - self::ARTICLE, - self::PRODUCT, - self::TRANSLATION, - - ); - } } diff --git a/tests/Gedmo/Translatable/Issue/Issue2152Test.php b/tests/Gedmo/Translatable/Issue/Issue2152Test.php new file mode 100644 index 0000000000..10bd9c3d80 --- /dev/null +++ b/tests/Gedmo/Translatable/Issue/Issue2152Test.php @@ -0,0 +1,98 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Issue; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Issue2152\EntityWithTranslatableBoolean; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; + +final class Issue2152Test extends BaseTestCaseORM +{ + private TranslatableListener $translatableListener; + + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + + $this->translatableListener = new TranslatableListener(); + $this->translatableListener->setTranslatableLocale('en'); + $this->translatableListener->setDefaultLocale('en'); + $this->translatableListener->setTranslationFallback(true); + $evm->addEventSubscriber($this->translatableListener); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testShouldFindInheritedClassTranslations(): void + { + // Arrange + // by default we have English + $title = 'Hello World'; + $isOperating = '1'; + + // operating in germany + $deTitle = 'Hallo Welt'; + $isOperatingInGermany = '0'; + + // but in Ukraine not operating, should fallback to default one + $uaTitle = null; + $isOperatingInUkraine = null; + + $entity = new EntityWithTranslatableBoolean($title, $isOperating); + $this->em->persist($entity); + $this->em->flush(); + + $entity->translateInLocale('de', $deTitle, $isOperatingInGermany); + + $this->em->persist($entity); + $this->em->flush(); + + $entity->translateInLocale('ua', $uaTitle, $isOperatingInUkraine); + + $this->em->persist($entity); + $this->em->flush(); + + // Act + $entityInDe = $this->findUsingQueryBuilder('de'); + $entityInUa = $this->findUsingQueryBuilder('ua'); + + // Assert + + static::assertSame($deTitle, $entityInDe->getTitle()); + static::assertSame($isOperatingInGermany, $entityInDe->isOperating()); + + static::assertSame($title, $entityInUa->getTitle(), 'should fallback to default title if null'); + static::assertSame($isOperating, $entityInUa->isOperating(), ' should fallback to default operating if null'); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Translation::class, + EntityWithTranslatableBoolean::class, + ]; + } + + private function findUsingQueryBuilder(string $locale): ?EntityWithTranslatableBoolean + { + $this->em->clear(); + $this->translatableListener->setTranslatableLocale($locale); + + $qb = $this->em->createQueryBuilder()->select('e')->from(EntityWithTranslatableBoolean::class, 'e'); + + return $qb->getQuery()->getSingleResult(); + } +} diff --git a/tests/Gedmo/Translatable/Issue/Issue2167Test.php b/tests/Gedmo/Translatable/Issue/Issue2167Test.php new file mode 100644 index 0000000000..44ab8e2e6f --- /dev/null +++ b/tests/Gedmo/Translatable/Issue/Issue2167Test.php @@ -0,0 +1,105 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Translatable\Issue; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Issue2167\Article; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; + +class Issue2167Test extends BaseTestCaseORM +{ + private TranslatableListener $translatableListener; + + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + + $this->translatableListener = new TranslatableListener(); + $this->translatableListener->setTranslatableLocale('en'); + $this->translatableListener->setDefaultLocale('en'); + $this->translatableListener->setTranslationFallback(false); + $evm->addEventSubscriber($this->translatableListener); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testShouldFindInheritedClassTranslations(): void + { + $enTitle = 'My english title'; + $deTitle = 'My german title'; + + // English + $entity = new Article(); + $entity->setTitle($enTitle); + $entity->setLocale('en'); + $this->em->persist($entity); + $this->em->flush(); + + // German + $entity->setLocale('de'); + $entity->setTitle($deTitle); + $this->em->flush(); + + // Find with default translation value as null value (default setting) + $entityInEn = $this->findUsingQueryBuilder('en'); + $entityInDe = $this->findUsingQueryBuilder('de'); + $entityInFr = $this->findUsingQueryBuilder('fr'); + + static::assertSame($enTitle, $entityInEn->getTitle()); + static::assertSame($deTitle, $entityInDe->getTitle()); + static::assertNull($entityInFr->getTitle()); + + // Find with default translation value as empty string + $this->translatableListener->setDefaultTranslationValue(''); + + $entityInEn = $this->findUsingQueryBuilder('en'); + $entityInDe = $this->findUsingQueryBuilder('de'); + $entityInFr = $this->findUsingQueryBuilder('fr'); + + static::assertSame($enTitle, $entityInEn->getTitle()); + static::assertSame($deTitle, $entityInDe->getTitle()); + static::assertSame('', $entityInFr->getTitle()); + + // Find with default translation value as not empty string + $this->translatableListener->setDefaultTranslationValue('no_translated'); + + $entityInEn = $this->findUsingQueryBuilder('en'); + $entityInDe = $this->findUsingQueryBuilder('de'); + $entityInFr = $this->findUsingQueryBuilder('fr'); + + static::assertSame($enTitle, $entityInEn->getTitle()); + static::assertSame($deTitle, $entityInDe->getTitle()); + static::assertSame('no_translated', $entityInFr->getTitle()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Translation::class, + Article::class, + ]; + } + + private function findUsingQueryBuilder(string $locale): ?Article + { + $this->em->clear(); + $this->translatableListener->setTranslatableLocale($locale); + + $qb = $this->em->createQueryBuilder()->select('e')->from(Article::class, 'e'); + + return $qb->getQuery()->getSingleResult(); + } +} diff --git a/tests/Gedmo/Translatable/Issue/Issue75Test.php b/tests/Gedmo/Translatable/Issue/Issue75Test.php deleted file mode 100644 index e783bc5940..0000000000 --- a/tests/Gedmo/Translatable/Issue/Issue75Test.php +++ /dev/null @@ -1,102 +0,0 @@ - - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) - */ -class Issue75Test extends BaseTestCaseORM -{ - const ARTICLE = 'Translatable\\Fixture\\Issue75\\Article'; - const IMAGE = 'Translatable\\Fixture\\Issue75\\Image'; - const FILE = 'Translatable\\Fixture\\Issue75\\File'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; - - private $translatableListener; - - protected function setUp() - { - parent::setUp(); - - $evm = new EventManager(); - $this->translatableListener = new TranslatableListener(); - $this->translatableListener->setTranslatableLocale('en'); - $this->translatableListener->setDefaultLocale('en'); - $evm->addEventSubscriber($this->translatableListener); - - $this->getMockSqliteEntityManager($evm); - } - - public function testIssue75() - { - $repo = $this->em->getRepository(self::TRANSLATION); - - // Step1: article creation in default locale - $image1 = new Image(); - $image1->setTitle('img1'); - $this->em->persist($image1); - - /*$image2 = new Image; - $image2->setTitle('img2'); - $this->em->persist($image2);*/ - - $article = new Article(); - $article->setTitle('en art'); - // images is not an array - //$article->setImages(array($image1, $image2)); - $this->em->persist($article); - - //$this->em->flush();*/ - $image2 = new Image(); //line 62 - $image2->setTitle('en img2'); - $this->em->persist($image2); - - $image32 = new Image(); // + - $image32->setTitle('en img3'); // + - $this->em->persist($image32); // + - - $article->addImage($image1); - $article->addImage($image2); - - $this->em->persist($article); // + - $this->em->flush(); - - $article->setTitle('nada'); // + - $article->addImage($image32); // + - $this->em->persist($article); // + - $this->em->flush(); - - //Step2: article update in another locale - $article = $this->em->find(self::ARTICLE, $article->getId()); - $image1 = $this->em->find(self::IMAGE, $image1->getId()); - $image2 = $this->em->find(self::IMAGE, $image2->getId()); - $article->setTitle('en updated'); - /** - * here you duplicate the objects in collection, it already - * contains them. Read more about doctrine collections - */ - $article->setImages(array($image1, $image2)); - $this->em->persist($article); - $this->em->flush(); - } - - protected function getUsedEntityFixtures() - { - return array( - self::ARTICLE, - self::TRANSLATION, - self::IMAGE, - self::FILE, - ); - } -} diff --git a/tests/Gedmo/Translatable/Issue/Issue84Test.php b/tests/Gedmo/Translatable/Issue/Issue84Test.php index aa40ff6340..cea21728f0 100644 --- a/tests/Gedmo/Translatable/Issue/Issue84Test.php +++ b/tests/Gedmo/Translatable/Issue/Issue84Test.php @@ -1,27 +1,32 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\Article; -use Doctrine\ORM\Proxy\Proxy; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Article; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class Issue84Test extends BaseTestCaseORM +final class Issue84Test extends BaseTestCaseORM { - const ARTICLE = 'Translatable\\Fixture\\Article'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; - - private $translatableListener; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -30,12 +35,12 @@ protected function setUp() $this->translatableListener->setTranslatableLocale('en'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testIssue84() + public function testIssue84(): void { - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $article = new Article(); $article->setTitle('en art'); @@ -44,18 +49,18 @@ public function testIssue84() $this->em->flush(); $this->em->clear(); - $article = $this->em->getReference(self::ARTICLE, 1); - $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $article); + $article = $this->em->getReference(Article::class, 1); + static::assertTrue($this->em->isUninitializedObject($article)); $trans = $repo->findTranslations($article); - $this->assertEquals(1, count($trans)); + static::assertCount(1, $trans); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::TRANSLATION, - ); + return [ + Article::class, + Translation::class, + ]; } } diff --git a/tests/Gedmo/Translatable/Issue/Issue922Test.php b/tests/Gedmo/Translatable/Issue/Issue922Test.php index 5b4a00ff28..1341f91df3 100644 --- a/tests/Gedmo/Translatable/Issue/Issue922Test.php +++ b/tests/Gedmo/Translatable/Issue/Issue922Test.php @@ -1,23 +1,30 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable\Issue; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\Issue922\Post; use Doctrine\ORM\Query; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Issue922\Post; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Hydrator\ORM\ObjectHydrator; use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; +use Gedmo\Translatable\TranslatableListener; -class Issue922Test extends BaseTestCaseORM +final class Issue922Test extends BaseTestCaseORM { - const POST = 'Translatable\Fixture\Issue922\Post'; - const TRANSLATION = 'Gedmo\Translatable\Entity\Translation'; - - const TREE_WALKER_TRANSLATION = 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'; - - private $translatableListener; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -28,13 +35,10 @@ protected function setUp() $this->translatableListener->setPersistDefaultLocaleTranslation(true); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldTranslateDateFields() + public function testShouldTranslateDateFields(): void { $p1 = new Post(); $p1->setPublishedAt(new \DateTime()); @@ -54,37 +58,35 @@ public function shouldTranslateDateFields() // clear and test postLoad event values set $this->em->clear(); - $p1 = $this->em->find(self::POST, $p1->getId()); - $this->assertInstanceOf('DateTime', $p1->getPublishedAt()); - $this->assertInstanceOf('DateTime', $p1->getTimestampAt()); - $this->assertInstanceOf('DateTime', $p1->getDateAt()); - $this->assertSame(false, $p1->getBoolean()); + $p1 = $this->em->find(Post::class, $p1->getId()); + static::assertInstanceOf('DateTime', $p1->getPublishedAt()); + static::assertInstanceOf('DateTime', $p1->getTimestampAt()); + static::assertInstanceOf('DateTime', $p1->getDateAt()); + static::assertFalse($p1->getBoolean()); // clear and test query hint hydration $this->em->clear(); - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\Translatable\Hydrator\ORM\ObjectHydrator')); - - $q = $this->em->createQuery('SELECT p FROM '.self::POST.' p'); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_OBJECT_TRANSLATION, + ObjectHydrator::class + ); + + $q = $this->em->createQuery('SELECT p FROM '.Post::class.' p'); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $q->setHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'de'); $p1 = $q->getSingleResult(); - $this->assertInstanceOf('DateTime', $p1->getPublishedAt()); - $this->assertInstanceOf('DateTime', $p1->getTimestampAt()); - $this->assertInstanceOf('DateTime', $p1->getDateAt()); - $this->assertSame(false, $p1->getBoolean()); + static::assertInstanceOf('DateTime', $p1->getPublishedAt()); + static::assertInstanceOf('DateTime', $p1->getTimestampAt()); + static::assertInstanceOf('DateTime', $p1->getDateAt()); + static::assertFalse($p1->getBoolean()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::POST, - self::TRANSLATION, - ); + return [ + Post::class, + Translation::class, + ]; } } diff --git a/tests/Gedmo/Translatable/MixedValueTranslationTest.php b/tests/Gedmo/Translatable/MixedValueTranslationTest.php index 44511cb258..96f126a8ed 100644 --- a/tests/Gedmo/Translatable/MixedValueTranslationTest.php +++ b/tests/Gedmo/Translatable/MixedValueTranslationTest.php @@ -1,32 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; use Doctrine\DBAL\Types\Type; -use Translatable\Fixture\MixedValue; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\MixedValue; +use Gedmo\Tests\Translatable\Fixture\Type\Custom; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MixedValueTranslationTest extends BaseTestCaseORM +final class MixedValueTranslationTest extends BaseTestCaseORM { - const MIXED = 'Translatable\\Fixture\\MixedValue'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; - - private $translatableListener; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); if (!Type::hasType('custom')) { - Type::addType('custom', 'Translatable\Fixture\Type\Custom'); + Type::addType('custom', Custom::class); } $evm = new EventManager(); @@ -35,24 +42,24 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en_us'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testFixtureGeneratedTranslations() + public function testFixtureGeneratedTranslations(): void { - $repo = $this->em->getRepository(self::MIXED); - $mixed = $repo->findOneById(1); + $repo = $this->em->getRepository(MixedValue::class); + $mixed = $repo->findOneBy(['id' => 1]); - $this->assertTrue($mixed->getDate() instanceof \DateTime); - $this->assertTrue($mixed->getCust() instanceof \stdClass); - $this->assertEquals('en', $mixed->getCust()->test); + static::assertInstanceOf(\DateTime::class, $mixed->getDate()); + static::assertInstanceOf(\stdClass::class, $mixed->getCust()); + static::assertSame('en', $mixed->getCust()->test); } - public function testOtherTranslation() + public function testOtherTranslation(): void { - $repo = $this->em->getRepository(self::MIXED); - $mixed = $repo->findOneById(1); + $repo = $this->em->getRepository(MixedValue::class); + $mixed = $repo->findOneBy(['id' => 1]); $this->translatableListener->setTranslatableLocale('de_de'); $mixed->setDate(new \DateTime('2000-00-00 00:00:00')); @@ -64,27 +71,27 @@ public function testOtherTranslation() $this->em->flush(); $this->em->clear(); - $mixed = $repo->findOneById(1); - $transRepo = $this->em->getRepository(self::TRANSLATION); + $mixed = $repo->findOneBy(['id' => 1]); + $transRepo = $this->em->getRepository(Translation::class); $translations = $transRepo->findTranslations($mixed); - $this->assertCount(1, $translations); - $this->assertArrayHasKey('de_de', $translations); + static::assertCount(1, $translations); + static::assertArrayHasKey('de_de', $translations); $cust = unserialize($translations['de_de']['cust']); - $this->assertTrue($cust instanceof \stdClass); - $this->assertEquals('de', $cust->test); + static::assertInstanceOf(\stdClass::class, $cust); + static::assertSame('de', $cust->test); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::MIXED, - self::TRANSLATION, - ); + return [ + MixedValue::class, + Translation::class, + ]; } - private function populate() + private function populate(): void { $mixedEn = new MixedValue(); $mixedEn->setDate(new \DateTime()); diff --git a/tests/Gedmo/Translatable/PersonalTranslationDocumentTest.php b/tests/Gedmo/Translatable/PersonalTranslationDocumentTest.php index afdaa2df9a..90bbc4c434 100644 --- a/tests/Gedmo/Translatable/PersonalTranslationDocumentTest.php +++ b/tests/Gedmo/Translatable/PersonalTranslationDocumentTest.php @@ -1,28 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Translatable\Fixture\Document\Personal\Article; -use Translatable\Fixture\Document\Personal\ArticleTranslation; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; +use Gedmo\Tests\Translatable\Fixture\Document\Personal\Article; +use Gedmo\Tests\Translatable\Fixture\Document\Personal\ArticleTranslation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class PersonalTranslationDocumentTest extends BaseTestCaseMongoODM +final class PersonalTranslationDocumentTest extends BaseTestCaseMongoODM { - const ARTICLE = 'Translatable\Fixture\Document\Personal\Article'; - const TRANSLATION = 'Translatable\Fixture\Document\Personal\ArticleTranslation'; + private TranslatableListener $translatableListener; - private $translatableListener; - private $id; + private ?string $id = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -32,34 +38,28 @@ protected function setUp() $this->translatableListener->setTranslatableLocale('en'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); } - /** - * @test - */ - public function shouldCreateTranslations() + public function testShouldCreateTranslations(): void { $this->populate(); - $article = $this->dm->getRepository(self::ARTICLE)->find($this->id); + $article = $this->dm->getRepository(Article::class)->find($this->id); $translations = $article->getTranslations(); - $this->assertCount(2, $translations); + static::assertCount(2, $translations); } - /** - * @test - */ - public function shouldTranslateTheRecord() + public function testShouldTranslateTheRecord(): void { $this->populate(); $this->translatableListener->setTranslatableLocale('lt'); - $article = $this->dm->getRepository(self::ARTICLE)->find($this->id); - $this->assertEquals('lt', $article->getTitle()); + $article = $this->dm->getRepository(Article::class)->find($this->id); + static::assertSame('lt', $article->getTitle()); } - private function populate() + private function populate(): void { $article = new Article(); $article->setTitle('en'); diff --git a/tests/Gedmo/Translatable/PersonalTranslationTest.php b/tests/Gedmo/Translatable/PersonalTranslationTest.php index 9abb275b1b..5383545775 100644 --- a/tests/Gedmo/Translatable/PersonalTranslationTest.php +++ b/tests/Gedmo/Translatable/PersonalTranslationTest.php @@ -1,30 +1,35 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; use Doctrine\Common\EventManager; +use Doctrine\DBAL\ParameterType; use Doctrine\ORM\Query; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Personal\Article; +use Gedmo\Tests\Translatable\Fixture\Personal\PersonalArticleTranslation; use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\Personal\Article; -use Translatable\Fixture\Personal\PersonalArticleTranslation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class PersonalTranslationTest extends BaseTestCaseORM +final class PersonalTranslationTest extends BaseTestCaseORM { - const ARTICLE = 'Translatable\Fixture\Personal\Article'; - const TRANSLATION = 'Translatable\Fixture\Personal\PersonalArticleTranslation'; - const TREE_WALKER_TRANSLATION = 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'; + private TranslatableListener $translatableListener; - private $translatableListener; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -33,77 +38,73 @@ protected function setUp() $this->translatableListener->setTranslatableLocale('en'); $this->translatableListener->setDefaultLocale('en'); $evm->addEventSubscriber($this->translatableListener); - - $conn = array( - 'driver' => 'pdo_mysql', - 'host' => '127.0.0.1', - 'dbname' => 'test', - 'user' => 'root', - 'password' => 'nimda', - ); - //$this->getMockCustomEntityManager($conn, $evm); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldPersistDefaultLocaleTranslationIfRequired() + public function testShouldPersistDefaultLocaleTranslationIfRequired(): void { $this->translatableListener->setPersistDefaultLocaleTranslation(true); $this->populate(); - $article = $this->em->find(self::ARTICLE, array('id' => 1)); + $article = $this->em->find(Article::class, ['id' => 1]); $translations = $article->getTranslations(); - $this->assertCount(3, $translations); + static::assertCount(3, $translations); } - /** - * @test - */ - public function shouldCreateTranslations() + public function testShouldCreateTranslations(): void { $this->populate(); - $article = $this->em->find(self::ARTICLE, array('id' => 1)); + $article = $this->em->find(Article::class, ['id' => 1]); $translations = $article->getTranslations(); - $this->assertCount(2, $translations); + static::assertCount(2, $translations); } - /** - * @test - */ - public function shouldTranslateTheRecord() + public function testShouldTranslateTheRecord(): void { $this->populate(); $this->translatableListener->setTranslatableLocale('lt'); - $this->startQueryLog(); - $article = $this->em->find(self::ARTICLE, array('id' => 1)); + $this->queryLogger->reset(); + + $article = $this->em->find(Article::class, ['id' => 1]); + + static::assertCount(2, $this->queryLogger->queries); + + static::assertSame([ + 'message' => 'Executing statement: {sql} (parameters: {params}, types: {types})', + 'context' => [ + 'sql' => 'SELECT t0.id AS id_1, t0.title AS title_2 FROM Article t0 WHERE t0.id = ?', + 'params' => [1 => 1], + 'types' => [1 => ParameterType::INTEGER], + ], + ], $this->queryLogger->queries[0]); - $sqlQueriesExecuted = $this->queryAnalyzer->getExecutedQueries(); - $this->assertCount(2, $sqlQueriesExecuted); - $this->assertEquals('SELECT t0.id AS id_1, t0.locale AS locale_2, t0.field AS field_3, t0.content AS content_4, t0.object_id AS object_id_5 FROM article_translations t0 WHERE t0.object_id = 1', $sqlQueriesExecuted[1]); - $this->assertEquals('lt', $article->getTitle()); + static::assertSame([ + 'message' => 'Executing statement: {sql} (parameters: {params}, types: {types})', + 'context' => [ + 'sql' => 'SELECT t0.id AS id_1, t0.locale AS locale_2, t0.field AS field_3, t0.content AS content_4, t0.object_id AS object_id_5 FROM article_translations t0 WHERE t0.object_id = ?', + 'params' => [1 => 1], + 'types' => [1 => ParameterType::INTEGER], + ], + ], $this->queryLogger->queries[1]); + + static::assertSame('lt', $article->getTitle()); } - /** - * @test - */ - public function shouldCascadeDeletionsByForeignKeyConstraints() + public function testShouldCascadeDeletionsByForeignKeyConstraints(): void { - if ($this->em->getConnection()->getDatabasePlatform()->getName() == 'sqlite') { - $this->markTestSkipped('Foreign key constraints does not map in sqlite.'); + // Uses normalized comparison due to case differences between versions + if ('doctrine\dbal\platforms\sqliteplatform' === strtolower(get_class($this->em->getConnection()->getDatabasePlatform()))) { + static::markTestSkipped('Foreign key constraints do not map in SQLite.'); } + $this->populate(); - $this->em->createQuery('DELETE FROM '.self::ARTICLE.' a')->getSingleScalarResult(); - $trans = $this->em->getRepository(self::TRANSLATION)->findAll(); + $this->em->createQuery('DELETE FROM '.Article::class.' a')->getSingleScalarResult(); + $trans = $this->em->getRepository(PersonalArticleTranslation::class)->findAll(); - $this->assertCount(0, $trans); + static::assertCount(0, $trans); } - /** - * @test - */ - public function shouldOverrideTranslationInEntityBeingTranslated() + public function testShouldOverrideTranslationInEntityBeingTranslated(): void { $this->translatableListener->setDefaultLocale('de'); $article = new Article(); @@ -120,16 +121,15 @@ public function shouldOverrideTranslationInEntityBeingTranslated() $this->em->persist($article); $this->em->flush(); - $trans = $this->em->createQuery('SELECT t FROM '.self::TRANSLATION.' t')->getArrayResult(); - $this->assertCount(1, $trans); - $this->assertEquals('override', $trans[0]['content']); + $trans = $this->em->createQuery('SELECT t FROM '.PersonalArticleTranslation::class.' t')->getArrayResult(); + static::assertCount(1, $trans); + static::assertSame('override', $trans[0]['content']); } /** * Covers issue #438 - * @test */ - public function shouldPersistDefaultLocaleValue() + public function testShouldPersistDefaultLocaleValue(): void { $this->translatableListener->setTranslatableLocale('de'); $article = new Article(); @@ -157,19 +157,16 @@ public function shouldPersistDefaultLocaleValue() $this->em->flush(); $this->translatableListener->setTranslatableLocale('en'); - $articles = $this->em->createQuery('SELECT t FROM '.self::ARTICLE.' t')->getArrayResult(); - $this->assertEquals('en', $articles[0]['title']); - $trans = $this->em->createQuery('SELECT t FROM '.self::TRANSLATION.' t')->getArrayResult(); - $this->assertCount(2, $trans); + $articles = $this->em->createQuery('SELECT t FROM '.Article::class.' t')->getArrayResult(); + static::assertSame('en', $articles[0]['title']); + $trans = $this->em->createQuery('SELECT t FROM '.PersonalArticleTranslation::class.' t')->getArrayResult(); + static::assertCount(2, $trans); foreach ($trans as $item) { - $this->assertEquals($item['locale'], $item['content']); + static::assertSame($item['locale'], $item['content']); } } - /** - * @test - */ - public function shouldFindFromIdentityMap() + public function testShouldFindFromIdentityMap(): void { $article = new Article(); $article->setTitle('en'); @@ -185,41 +182,78 @@ public function shouldFindFromIdentityMap() $this->em->persist($article); $this->em->flush(); - $this->startQueryLog(); + $this->queryLogger->reset(); + $this->translatableListener->setTranslatableLocale('lt'); $article->setTitle('change lt'); $this->em->persist($article); $this->em->flush(); - $sqlQueriesExecuted = $this->queryAnalyzer->getExecutedQueries(); - $this->assertCount(3, $sqlQueriesExecuted); // one update, transaction start - commit - $this->assertEquals("UPDATE article_translations SET content = 'change lt' WHERE id = 1", $sqlQueriesExecuted[1]); + + static::assertCount(3, $this->queryLogger->queries); + + static::assertSame([ + 'message' => 'Beginning transaction', + 'context' => [], + ], $this->queryLogger->queries[0]); + + static::assertSame([ + 'message' => 'Executing statement: {sql} (parameters: {params}, types: {types})', + 'context' => [ + 'sql' => 'UPDATE article_translations SET content = ? WHERE id = ?', + 'params' => [ + 1 => 'change lt', + 2 => 1, + ], + 'types' => [ + 1 => ParameterType::STRING, + 2 => ParameterType::INTEGER, + ], + ], + ], $this->queryLogger->queries[1]); + + static::assertSame([ + 'message' => 'Committing transaction', + 'context' => [], + ], $this->queryLogger->queries[2]); } - /** - * @test - */ - public function shouldBeAbleToUseTranslationQueryHint() + public function testShouldBeAbleToUseTranslationQueryHint(): void { $this->populate(); - $dql = 'SELECT a.title FROM '.self::ARTICLE.' a'; + $dql = 'SELECT a.title FROM '.Article::class.' a'; $query = $this ->em->createQuery($dql) - ->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION) + ->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class) ->setHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'lt') ; - $this->startQueryLog(); + $this->queryLogger->reset(); + $result = $query->getArrayResult(); - $this->assertCount(1, $result); - $this->assertEquals('lt', $result[0]['title']); - $sqlQueriesExecuted = $this->queryAnalyzer->getExecutedQueries(); - $this->assertCount(1, $sqlQueriesExecuted); - $this->assertEquals("SELECT CAST(t1_.content AS VARCHAR(128)) AS title_0 FROM Article a0_ LEFT JOIN article_translations t1_ ON t1_.locale = 'lt' AND t1_.field = 'title' AND t1_.object_id = a0_.id", $sqlQueriesExecuted[0]); + static::assertCount(1, $result); + static::assertSame('lt', $result[0]['title']); + + static::assertCount(1, $this->queryLogger->queries); + + static::assertSame([ + 'message' => 'Executing query: {sql}', + 'context' => [ + 'sql' => "SELECT CAST(t1_.content AS VARCHAR(128)) AS title_0 FROM Article a0_ LEFT JOIN article_translations t1_ ON t1_.locale = 'lt' AND t1_.field = 'title' AND t1_.object_id = a0_.id", + ], + ], $this->queryLogger->queries[0]); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + PersonalArticleTranslation::class, + ]; } - private function populate() + private function populate(): void { $article = new Article(); $article->setTitle('en'); @@ -242,12 +276,4 @@ private function populate() $this->em->flush(); $this->em->clear(); } - - protected function getUsedEntityFixtures() - { - return array( - self::ARTICLE, - self::TRANSLATION, - ); - } } diff --git a/tests/Gedmo/Translatable/TranslatableDocumentCollectionTest.php b/tests/Gedmo/Translatable/TranslatableDocumentCollectionTest.php index c099e90040..7a8e789168 100644 --- a/tests/Gedmo/Translatable/TranslatableDocumentCollectionTest.php +++ b/tests/Gedmo/Translatable/TranslatableDocumentCollectionTest.php @@ -1,27 +1,35 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Translatable\Fixture\Document\SimpleArticle as Article; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; +use Gedmo\Tests\Translatable\Fixture\Document\SimpleArticle as Article; +use Gedmo\Translatable\Document\Repository\TranslationRepository; +use Gedmo\Translatable\Document\Translation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableDocumentCollectionTest extends BaseTestCaseMongoODM +final class TranslatableDocumentCollectionTest extends BaseTestCaseMongoODM { - const ARTICLE = 'Translatable\\Fixture\\Document\\SimpleArticle'; - const TRANSLATION = 'Gedmo\\Translatable\\Document\\Translation'; + private TranslatableListener $translatableListener; - private $translatableListener; - private $id; + private ?string $id = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -31,39 +39,35 @@ protected function setUp() $this->translatableListener->setTranslatableLocale('en_us'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); $this->populate(); } - /** - * @test - */ - public function shouldPersistMultipleTranslations() + public function testShouldPersistMultipleTranslations(): void { - $repo = $this->dm->getRepository(self::TRANSLATION); - $sport = $this->dm->getRepository(self::ARTICLE)->find($this->id); + $repo = $this->dm->getRepository(Translation::class); + static::assertInstanceOf(TranslationRepository::class, $repo); + $sport = $this->dm->getRepository(Article::class)->find($this->id); $translations = $repo->findTranslations($sport); - $this->assertArrayHasKey('de_de', $translations); - $this->assertArrayHasKey('title', $translations['de_de']); - $this->assertArrayHasKey('content', $translations['de_de']); - $this->assertEquals('sport de', $translations['de_de']['title']); - $this->assertEquals('content de', $translations['de_de']['content']); - - $this->assertArrayHasKey('ru_ru', $translations); - $this->assertArrayHasKey('title', $translations['ru_ru']); - $this->assertArrayHasKey('content', $translations['ru_ru']); - $this->assertEquals('sport ru', $translations['ru_ru']['title']); - $this->assertEquals('content ru', $translations['ru_ru']['content']); + static::assertArrayHasKey('de_de', $translations); + static::assertArrayHasKey('title', $translations['de_de']); + static::assertArrayHasKey('content', $translations['de_de']); + static::assertSame('sport de', $translations['de_de']['title']); + static::assertSame('content de', $translations['de_de']['content']); + + static::assertArrayHasKey('ru_ru', $translations); + static::assertArrayHasKey('title', $translations['ru_ru']); + static::assertArrayHasKey('content', $translations['ru_ru']); + static::assertSame('sport ru', $translations['ru_ru']['title']); + static::assertSame('content ru', $translations['ru_ru']['content']); } - /** - * @test - */ - public function shouldUpdateTranslation() + public function testShouldUpdateTranslation(): void { - $repo = $this->dm->getRepository(self::TRANSLATION); - $sport = $this->dm->getRepository(self::ARTICLE)->find($this->id); + $repo = $this->dm->getRepository(Translation::class); + static::assertInstanceOf(TranslationRepository::class, $repo); + $sport = $this->dm->getRepository(Article::class)->find($this->id); $repo ->translate($sport, 'title', 'ru_ru', 'sport ru change') ->translate($sport, 'content', 'ru_ru', 'content ru change') @@ -71,22 +75,20 @@ public function shouldUpdateTranslation() $this->dm->flush(); $translations = $repo->findTranslations($sport); - $this->assertCount(2, $translations); + static::assertCount(2, $translations); - $this->assertArrayHasKey('ru_ru', $translations); - $this->assertArrayHasKey('title', $translations['ru_ru']); - $this->assertArrayHasKey('content', $translations['ru_ru']); - $this->assertEquals('sport ru change', $translations['ru_ru']['title']); - $this->assertEquals('content ru change', $translations['ru_ru']['content']); + static::assertArrayHasKey('ru_ru', $translations); + static::assertArrayHasKey('title', $translations['ru_ru']); + static::assertArrayHasKey('content', $translations['ru_ru']); + static::assertSame('sport ru change', $translations['ru_ru']['title']); + static::assertSame('content ru change', $translations['ru_ru']['content']); } - /** - * @test - */ - public function shouldUpdateMultipleTranslations() + public function testShouldUpdateMultipleTranslations(): void { - $repo = $this->dm->getRepository(self::TRANSLATION); - $sport = $this->dm->getRepository(self::ARTICLE)->find($this->id); + $repo = $this->dm->getRepository(Translation::class); + static::assertInstanceOf(TranslationRepository::class, $repo); + $sport = $this->dm->getRepository(Article::class)->find($this->id); $sport->setTitle('Changed'); $repo ->translate($sport, 'title', 'lt_lt', 'sport lt') @@ -99,33 +101,34 @@ public function shouldUpdateMultipleTranslations() $this->dm->flush(); - $this->assertEquals('sport en update', $sport->getTitle()); - $this->assertEquals('content en update', $sport->getContent()); + static::assertSame('sport en update', $sport->getTitle()); + static::assertSame('content en update', $sport->getContent()); $translations = $repo->findTranslations($sport); - $this->assertArrayHasKey('de_de', $translations); - $this->assertArrayHasKey('title', $translations['de_de']); - $this->assertArrayHasKey('content', $translations['de_de']); - $this->assertEquals('sport de', $translations['de_de']['title']); - $this->assertEquals('content de', $translations['de_de']['content']); - - $this->assertArrayHasKey('ru_ru', $translations); - $this->assertArrayHasKey('title', $translations['ru_ru']); - $this->assertArrayHasKey('content', $translations['ru_ru']); - $this->assertEquals('sport ru change', $translations['ru_ru']['title']); - $this->assertEquals('content ru change', $translations['ru_ru']['content']); - - $this->assertArrayHasKey('lt_lt', $translations); - $this->assertArrayHasKey('title', $translations['lt_lt']); - $this->assertArrayHasKey('content', $translations['lt_lt']); - $this->assertEquals('sport lt', $translations['lt_lt']['title']); - $this->assertEquals('content lt', $translations['lt_lt']['content']); + static::assertArrayHasKey('de_de', $translations); + static::assertArrayHasKey('title', $translations['de_de']); + static::assertArrayHasKey('content', $translations['de_de']); + static::assertSame('sport de', $translations['de_de']['title']); + static::assertSame('content de', $translations['de_de']['content']); + + static::assertArrayHasKey('ru_ru', $translations); + static::assertArrayHasKey('title', $translations['ru_ru']); + static::assertArrayHasKey('content', $translations['ru_ru']); + static::assertSame('sport ru change', $translations['ru_ru']['title']); + static::assertSame('content ru change', $translations['ru_ru']['content']); + + static::assertArrayHasKey('lt_lt', $translations); + static::assertArrayHasKey('title', $translations['lt_lt']); + static::assertArrayHasKey('content', $translations['lt_lt']); + static::assertSame('sport lt', $translations['lt_lt']['title']); + static::assertSame('content lt', $translations['lt_lt']['content']); } - private function populate() + private function populate(): void { - $repo = $this->dm->getRepository(self::TRANSLATION); + $repo = $this->dm->getRepository(Translation::class); + static::assertInstanceOf(TranslationRepository::class, $repo); $sport = new Article(); $sport->setTitle('Sport'); $sport->setContent('about sport'); diff --git a/tests/Gedmo/Translatable/TranslatableDocumentTest.php b/tests/Gedmo/Translatable/TranslatableDocumentTest.php index bacdce07e3..576fc7bdb8 100644 --- a/tests/Gedmo/Translatable/TranslatableDocumentTest.php +++ b/tests/Gedmo/Translatable/TranslatableDocumentTest.php @@ -1,28 +1,36 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; -use Tool\BaseTestCaseMongoODM; -use Gedmo\Sluggable\SluggableListener; use Doctrine\Common\EventManager; -use Translatable\Fixture\Document\Article; +use Gedmo\Sluggable\SluggableListener; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; +use Gedmo\Tests\Translatable\Fixture\Document\Article; +use Gedmo\Translatable\Document\Repository\TranslationRepository; +use Gedmo\Translatable\Document\Translation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for Translatable behavior ODM implementation * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableDocumentTest extends BaseTestCaseMongoODM +final class TranslatableDocumentTest extends BaseTestCaseMongoODM { - const ARTICLE = 'Translatable\\Fixture\\Document\\Article'; - const TRANSLATION = 'Gedmo\\Translatable\\Document\\Translation'; + private TranslatableListener $translatableListener; - private $translatableListener; - private $articleId; + private ?string $articleId = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); @@ -32,21 +40,21 @@ protected function setUp() $evm->addEventSubscriber(new SluggableListener()); $evm->addEventSubscriber($this->translatableListener); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); $this->populate(); } - public function testTranslation() + public function testTranslation(): void { // test inserted translations - $repo = $this->dm->getRepository(self::ARTICLE); - /*$article = $repo->findOneByTitle('Title EN'); + $repo = $this->dm->getRepository(Article::class); + $article = $repo->findOneBy(['title' => 'Title EN']); - $transRepo = $this->dm->getRepository(self::TRANSLATION); - $this->assertTrue($transRepo instanceof Document\Repository\TranslationRepository); + $transRepo = $this->dm->getRepository(Translation::class); + static::assertInstanceOf(TranslationRepository::class, $transRepo); $translations = $transRepo->findTranslations($article); - $this->assertCount(0, $translations); + static::assertCount(0, $translations); // test second translations $this->translatableListener->setTranslatableLocale('de_de'); @@ -59,37 +67,37 @@ public function testTranslation() $article = $repo->find($this->articleId); $translations = $transRepo->findTranslations($article); - $this->assertCount(1, $translations); + static::assertCount(1, $translations); - $this->assertArrayHasKey('de_de', $translations); - $this->assertArrayHasKey('title', $translations['de_de']); - $this->assertEquals('Title DE', $translations['de_de']['title']); + static::assertArrayHasKey('de_de', $translations); + static::assertArrayHasKey('title', $translations['de_de']); + static::assertSame('Title DE', $translations['de_de']['title']); - $this->assertArrayHasKey('code', $translations['de_de']); - $this->assertEquals('Code DE', $translations['de_de']['code']); + static::assertArrayHasKey('code', $translations['de_de']); + static::assertSame('Code DE', $translations['de_de']['code']); - $this->assertArrayHasKey('slug', $translations['de_de']); - $this->assertEquals('title-de-code-de', $translations['de_de']['slug']); + static::assertArrayHasKey('slug', $translations['de_de']); + static::assertSame('title-de-code-de', $translations['de_de']['slug']); // test value update - $this->dm->clear();*/ + $this->dm->clear(); $this->translatableListener->setTranslatableLocale('en_us'); $article = $repo->find($this->articleId); - $this->assertEquals('Title EN', $article->getTitle()); - $this->assertEquals('Code EN', $article->getCode()); - $this->assertEquals('title-en-code-en', $article->getSlug()); + static::assertSame('Title EN', $article->getTitle()); + static::assertSame('Code EN', $article->getCode()); + static::assertSame('title-en-code-en', $article->getSlug()); // test translation update - /*$article->setTitle('Title EN Updated'); + $article->setTitle('Title EN Updated'); $article->setCode('Code EN Updated'); $this->dm->persist($article); $this->dm->flush(); $this->dm->clear(); $article = $repo->find($this->articleId); - $this->assertEquals('Title EN Updated', $article->getTitle()); - $this->assertEquals('Code EN Updated', $article->getCode()); + static::assertSame('Title EN Updated', $article->getTitle()); + static::assertSame('Code EN Updated', $article->getCode()); // test removal of translations $this->dm->remove($article); @@ -97,13 +105,40 @@ public function testTranslation() $this->dm->clear(); $article = $repo->find($this->articleId); - $this->assertNull($article); + static::assertNull($article); $translations = $transRepo->findTranslationsByObjectId($this->articleId); - $this->assertCount(0, $translations);*/ + static::assertCount(0, $translations); + } + + public function testFindObjectByTranslatedField(): void + { + $repo = $this->dm->getRepository(Article::class); + $article = $repo->findOneBy(['title' => 'Title EN']); + static::assertInstanceOf(Article::class, $article); + + $this->translatableListener->setTranslatableLocale('de_de'); + $article->setTitle('Title DE'); + $article->setCode('Code DE'); + + $this->dm->persist($article); + $this->dm->flush(); + $this->dm->clear(); + + $transRepo = $this->dm->getRepository(Translation::class); + static::assertInstanceOf(TranslationRepository::class, $transRepo); + + $articleFound = $transRepo->findObjectByTranslatedField( + 'title', + 'Title DE', + Article::class + ); + static::assertInstanceOf(Article::class, $articleFound); + + static::assertSame($article->getId(), $articleFound->getId()); } - private function populate() + private function populate(): void { $art0 = new Article(); $art0->setTitle('Title EN'); diff --git a/tests/Gedmo/Translatable/TranslatableEntityCollectionTest.php b/tests/Gedmo/Translatable/TranslatableEntityCollectionTest.php index 46edb2a22e..78196bf92d 100644 --- a/tests/Gedmo/Translatable/TranslatableEntityCollectionTest.php +++ b/tests/Gedmo/Translatable/TranslatableEntityCollectionTest.php @@ -1,28 +1,33 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\Article; -use Translatable\Fixture\Comment; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Article; +use Gedmo\Tests\Translatable\Fixture\Comment; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableEntityCollectionTest extends BaseTestCaseORM +final class TranslatableEntityCollectionTest extends BaseTestCaseORM { - const ARTICLE = 'Translatable\\Fixture\\Article'; - const COMMENT = 'Translatable\\Fixture\\Comment'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; + private TranslatableListener $translatableListener; - private $translatableListener; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -32,26 +37,15 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en_us'); $evm->addEventSubscriber($this->translatableListener); - $conn = array( - 'driver' => 'pdo_mysql', - 'host' => '127.0.0.1', - 'dbname' => 'test', - 'user' => 'root', - 'password' => 'nimda', - ); - //$this->getMockCustomEntityManager($conn, $evm); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldEnsureSolvedIssue234() + public function testShouldEnsureSolvedIssue234(): void { $this->translatableListener->setTranslatableLocale('de'); $this->translatableListener->setDefaultLocale('en'); $this->translatableListener->setPersistDefaultLocaleTranslation(true); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $entity = new Article(); $entity->setTitle('he'); // is translated to de @@ -65,47 +59,41 @@ public function shouldEnsureSolvedIssue234() $this->em->persist($entity); $this->em->flush(); $this->em->clear(); - $trans = $repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(4, $trans); - $this->assertSame('my article de', $trans['de']['title']); // overrides "he" which would be used if translate for de not called - $this->assertSame('my article es', $trans['es']['title']); - $this->assertSame('my article fr', $trans['fr']['title']); - $this->assertSame('my article en', $trans['en']['title']); + $trans = $repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(4, $trans); + static::assertSame('my article de', $trans['de']['title']); // overrides "he" which would be used if translate for de not called + static::assertSame('my article es', $trans['es']['title']); + static::assertSame('my article fr', $trans['fr']['title']); + static::assertSame('my article en', $trans['en']['title']); } - /** - * @test - */ - public function shouldPersistMultipleTranslations() + public function testShouldPersistMultipleTranslations(): void { $this->populate(); - $repo = $this->em->getRepository(self::TRANSLATION); - $sport = $this->em->getRepository(self::ARTICLE)->find(1); + $repo = $this->em->getRepository(Translation::class); + $sport = $this->em->getRepository(Article::class)->find(1); $translations = $repo->findTranslations($sport); - $this->assertCount(2, $translations); + static::assertCount(2, $translations); - $this->assertArrayHasKey('de_de', $translations); - $this->assertArrayHasKey('title', $translations['de_de']); - $this->assertArrayHasKey('content', $translations['de_de']); - $this->assertEquals('sport de', $translations['de_de']['title']); - $this->assertEquals('content de', $translations['de_de']['content']); + static::assertArrayHasKey('de_de', $translations); + static::assertArrayHasKey('title', $translations['de_de']); + static::assertArrayHasKey('content', $translations['de_de']); + static::assertSame('sport de', $translations['de_de']['title']); + static::assertSame('content de', $translations['de_de']['content']); - $this->assertArrayHasKey('ru_ru', $translations); - $this->assertArrayHasKey('title', $translations['ru_ru']); - $this->assertArrayHasKey('content', $translations['ru_ru']); - $this->assertEquals('sport ru', $translations['ru_ru']['title']); - $this->assertEquals('content ru', $translations['ru_ru']['content']); + static::assertArrayHasKey('ru_ru', $translations); + static::assertArrayHasKey('title', $translations['ru_ru']); + static::assertArrayHasKey('content', $translations['ru_ru']); + static::assertSame('sport ru', $translations['ru_ru']['title']); + static::assertSame('content ru', $translations['ru_ru']['content']); } - /** - * @test - */ - public function shouldUpdateTranslation() + public function testShouldUpdateTranslation(): void { $this->populate(); - $repo = $this->em->getRepository(self::TRANSLATION); - $sport = $this->em->getRepository(self::ARTICLE)->find(1); + $repo = $this->em->getRepository(Translation::class); + $sport = $this->em->getRepository(Article::class)->find(1); $repo ->translate($sport, 'title', 'ru_ru', 'sport ru change') ->translate($sport, 'content', 'ru_ru', 'content ru change') @@ -113,23 +101,20 @@ public function shouldUpdateTranslation() $this->em->flush(); $translations = $repo->findTranslations($sport); - $this->assertCount(2, $translations); + static::assertCount(2, $translations); - $this->assertArrayHasKey('ru_ru', $translations); - $this->assertArrayHasKey('title', $translations['ru_ru']); - $this->assertArrayHasKey('content', $translations['ru_ru']); - $this->assertEquals('sport ru change', $translations['ru_ru']['title']); - $this->assertEquals('content ru change', $translations['ru_ru']['content']); + static::assertArrayHasKey('ru_ru', $translations); + static::assertArrayHasKey('title', $translations['ru_ru']); + static::assertArrayHasKey('content', $translations['ru_ru']); + static::assertSame('sport ru change', $translations['ru_ru']['title']); + static::assertSame('content ru change', $translations['ru_ru']['content']); } - /** - * @test - */ - public function shouldUpdateMultipleTranslations() + public function testShouldUpdateMultipleTranslations(): void { $this->populate(); - $repo = $this->em->getRepository(self::TRANSLATION); - $sport = $this->em->getRepository(self::ARTICLE)->find(1); + $repo = $this->em->getRepository(Translation::class); + $sport = $this->em->getRepository(Article::class)->find(1); $repo ->translate($sport, 'title', 'lt_lt', 'sport lt') ->translate($sport, 'content', 'lt_lt', 'content lt') @@ -140,34 +125,43 @@ public function shouldUpdateMultipleTranslations() ; $this->em->flush(); - $this->assertEquals('sport en update', $sport->getTitle()); - $this->assertEquals('content en update', $sport->getContent()); + static::assertSame('sport en update', $sport->getTitle()); + static::assertSame('content en update', $sport->getContent()); $translations = $repo->findTranslations($sport); - $this->assertCount(3, $translations); - - $this->assertArrayHasKey('de_de', $translations); - $this->assertArrayHasKey('title', $translations['de_de']); - $this->assertArrayHasKey('content', $translations['de_de']); - $this->assertEquals('sport de', $translations['de_de']['title']); - $this->assertEquals('content de', $translations['de_de']['content']); - - $this->assertArrayHasKey('ru_ru', $translations); - $this->assertArrayHasKey('title', $translations['ru_ru']); - $this->assertArrayHasKey('content', $translations['ru_ru']); - $this->assertEquals('sport ru change', $translations['ru_ru']['title']); - $this->assertEquals('content ru change', $translations['ru_ru']['content']); - - $this->assertArrayHasKey('lt_lt', $translations); - $this->assertArrayHasKey('title', $translations['lt_lt']); - $this->assertArrayHasKey('content', $translations['lt_lt']); - $this->assertEquals('sport lt', $translations['lt_lt']['title']); - $this->assertEquals('content lt', $translations['lt_lt']['content']); + static::assertCount(3, $translations); + + static::assertArrayHasKey('de_de', $translations); + static::assertArrayHasKey('title', $translations['de_de']); + static::assertArrayHasKey('content', $translations['de_de']); + static::assertSame('sport de', $translations['de_de']['title']); + static::assertSame('content de', $translations['de_de']['content']); + + static::assertArrayHasKey('ru_ru', $translations); + static::assertArrayHasKey('title', $translations['ru_ru']); + static::assertArrayHasKey('content', $translations['ru_ru']); + static::assertSame('sport ru change', $translations['ru_ru']['title']); + static::assertSame('content ru change', $translations['ru_ru']['content']); + + static::assertArrayHasKey('lt_lt', $translations); + static::assertArrayHasKey('title', $translations['lt_lt']); + static::assertArrayHasKey('content', $translations['lt_lt']); + static::assertSame('sport lt', $translations['lt_lt']['title']); + static::assertSame('content lt', $translations['lt_lt']['content']); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + Translation::class, + Comment::class, + ]; } - private function populate() + private function populate(): void { - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $sport = new Article(); $sport->setTitle('Sport'); $sport->setContent('about sport'); @@ -182,13 +176,4 @@ private function populate() $this->em->persist($sport); $this->em->flush(); } - - protected function getUsedEntityFixtures() - { - return array( - self::ARTICLE, - self::TRANSLATION, - self::COMMENT, - ); - } } diff --git a/tests/Gedmo/Translatable/TranslatableEntityDefaultTranslationTest.php b/tests/Gedmo/Translatable/TranslatableEntityDefaultTranslationTest.php index e61eaca6dd..afd9549d83 100644 --- a/tests/Gedmo/Translatable/TranslatableEntityDefaultTranslationTest.php +++ b/tests/Gedmo/Translatable/TranslatableEntityDefaultTranslationTest.php @@ -1,28 +1,39 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\Article; -use Translatable\Fixture\Comment; +use Doctrine\ORM\EntityRepository; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Article; +use Gedmo\Translatable\Entity\Repository\TranslationRepository; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableEntityDefaultTranslationTest extends BaseTestCaseORM +final class TranslatableEntityDefaultTranslationTest extends BaseTestCaseORM { - const ARTICLE = 'Translatable\\Fixture\\Article'; - const COMMENT = 'Translatable\\Fixture\\Comment'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; + private TranslatableListener $translatableListener; - private $translatableListener; + /** + * @var TranslationRepository + */ + private EntityRepository $repo; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -32,125 +43,115 @@ protected function setUp() $this->translatableListener->setDefaultLocale('defaultLocale'); $evm->addEventSubscriber($this->translatableListener); - $conn = array( - 'driver' => 'pdo_mysql', - 'host' => '127.0.0.1', - 'dbname' => 'test', - 'user' => 'root', - 'password' => 'nimda', - ); - //$this->getMockCustomEntityManager($conn, $evm); - $this->getMockSqliteEntityManager($evm); - - $this->repo = $this->em->getRepository(self::TRANSLATION); + $this->getDefaultMockSqliteEntityManager($evm); + + $this->repo = $this->em->getRepository(Translation::class); } // --- Tests for default translation overruling the translated entity // property ------------------------------------------------------------ - - public function testTranslatedPropertyWithoutPersistingDefault() + public function testTranslatedPropertyWithoutPersistingDefault(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( false ); + $this->translatableListener->setPersistDefaultLocaleTranslation(false); $entity = new Article(); $this->repo - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') ; - $this->assertSame('title translatedLocale', $entity->getTitle()); + static::assertSame('title translatedLocale', $entity->getTitle()); } - public function testTranslatedPropertyWithoutPersistingDefaultResorted() + public function testTranslatedPropertyWithoutPersistingDefaultResorted(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( false ); + $this->translatableListener->setPersistDefaultLocaleTranslation(false); $entity = new Article(); $this->repo ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ; - $this->assertSame('title translatedLocale', $entity->getTitle()); + static::assertSame('title translatedLocale', $entity->getTitle()); } - public function testTranslatedPropertyWithPersistingDefault() + public function testTranslatedPropertyWithPersistingDefault(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( true ); + $this->translatableListener->setPersistDefaultLocaleTranslation(true); $entity = new Article(); $this->repo - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') ; - $this->assertSame('title translatedLocale', $entity->getTitle()); + static::assertSame('title translatedLocale', $entity->getTitle()); } - public function testTranslatedPropertyWithPersistingDefaultResorted() + public function testTranslatedPropertyWithPersistingDefaultResorted(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( true ); + $this->translatableListener->setPersistDefaultLocaleTranslation(true); $entity = new Article(); $this->repo ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ; - $this->assertSame('title translatedLocale', $entity->getTitle()); + static::assertSame('title translatedLocale', $entity->getTitle()); } // --- Tests for default translation making it into the entity's // database row -------------------------------------------------------- - - public function testOnlyDefaultTranslationWithoutPersistingDefault() + public function testOnlyDefaultTranslationWithoutPersistingDefault(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( false ); + $this->translatableListener->setPersistDefaultLocaleTranslation(false); $entity = new Article(); $this->repo - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ; $this->em->persist($entity); $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(0, $trans); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(0, $trans); - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title defaultLocale', $articles[0]['title']); + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title defaultLocale', $articles[0]['title']); } - public function testOnlyDefaultTranslationWithPersistingDefault() + public function testOnlyDefaultTranslationWithPersistingDefault(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( true ); + $this->translatableListener->setPersistDefaultLocaleTranslation(true); $entity = new Article(); $this->repo - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ; $this->em->persist($entity); $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(1, $trans); - $this->assertSame('title defaultLocale', $trans['defaultLocale']['title']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(1, $trans); + static::assertSame('title defaultLocale', $trans['defaultLocale']['title']); - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title defaultLocale', $articles[0]['title']); + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title defaultLocale', $articles[0]['title']); } - public function testUpdateTranslationInDefaultLocale() + public function testUpdateTranslationInDefaultLocale(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( false ); + $this->translatableListener->setPersistDefaultLocaleTranslation(false); $entity = new Article(); $this->repo - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale'); $this->em->persist($entity); $this->em->flush(); $this->em->clear(); - $entity = $this->em->find(self::ARTICLE, 1); + $entity = $this->em->find(Article::class, 1); $entity->setTranslatableLocale('translatedLocale'); $this->em->refresh($entity); @@ -159,29 +160,29 @@ public function testUpdateTranslationInDefaultLocale() $this->em->flush(); - $qb = $this->em->createQueryBuilder('a'); + $qb = $this->em->createQueryBuilder(); $qb->select('a') - ->from(self::ARTICLE, 'a') + ->from(Article::class, 'a') ->where('a.id = 1'); $fields = $qb->getQuery()->getArrayResult(); - $this->assertEquals( 'update title defaultLocale', $fields[0]['title']); + static::assertSame('update title defaultLocale', $fields[0]['title']); } - public function testUpdateTranslationWithPersistingInDefaultLocale() + public function testUpdateTranslationWithPersistingInDefaultLocale(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( true ); + $this->translatableListener->setPersistDefaultLocaleTranslation(true); $entity = new Article(); $this->repo - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale'); $this->em->persist($entity); $this->em->flush(); $this->em->clear(); - $entity = $this->em->find(self::ARTICLE, 1); + $entity = $this->em->find(Article::class, 1); $entity->setTranslatableLocale('translatedLocale'); $this->em->refresh($entity); @@ -190,23 +191,23 @@ public function testUpdateTranslationWithPersistingInDefaultLocale() $this->em->flush(); - $qb = $this->em->createQueryBuilder('a'); + $qb = $this->em->createQueryBuilder(); $qb->select('a') - ->from(self::ARTICLE, 'a') + ->from(Article::class, 'a') ->where('a.id = 1'); $fields = $qb->getQuery()->getArrayResult(); - $this->assertEquals( 'update title defaultLocale', $fields[0]['title']); + static::assertSame('update title defaultLocale', $fields[0]['title']); } /** * As this test does not provide a default translation, we assert * that a translated value is picked as default value */ - public function testOnlyEntityTranslationWithoutPersistingDefault() + public function testOnlyEntityTranslationWithoutPersistingDefault(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( false ); + $this->translatableListener->setPersistDefaultLocaleTranslation(false); $entity = new Article(); $this->repo ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') @@ -216,22 +217,22 @@ public function testOnlyEntityTranslationWithoutPersistingDefault() $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(1, $trans); - $this->assertSame('title translatedLocale', $trans['translatedLocale']['title']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(1, $trans); + static::assertSame('title translatedLocale', $trans['translatedLocale']['title']); - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title translatedLocale', $articles[0]['title']); + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title translatedLocale', $articles[0]['title']); } /** * As this test does not provide a default translation, we assert * that a translated value is picked as default value */ - public function testOnlyEntityTranslationWithPersistingDefault() + public function testOnlyEntityTranslationWithPersistingDefault(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( true ); + $this->translatableListener->setPersistDefaultLocaleTranslation(true); $entity = new Article(); $this->repo ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') @@ -241,21 +242,21 @@ public function testOnlyEntityTranslationWithPersistingDefault() $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(1, $trans); - $this->assertSame('title translatedLocale', $trans['translatedLocale']['title']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(1, $trans); + static::assertSame('title translatedLocale', $trans['translatedLocale']['title']); - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title translatedLocale', $articles[0]['title']); + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title translatedLocale', $articles[0]['title']); } - public function testDefaultAndEntityTranslationWithoutPersistingDefault() + public function testDefaultAndEntityTranslationWithoutPersistingDefault(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( false ); + $this->translatableListener->setPersistDefaultLocaleTranslation(false); $entity = new Article(); $this->repo - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') ; @@ -263,43 +264,43 @@ public function testDefaultAndEntityTranslationWithoutPersistingDefault() $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(1, $trans); - $this->assertSame('title translatedLocale', $trans['translatedLocale']['title']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(1, $trans); + static::assertSame('title translatedLocale', $trans['translatedLocale']['title']); - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title defaultLocale', $articles[0]['title']); + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title defaultLocale', $articles[0]['title']); } - public function testDefaultAndEntityTranslationWithoutPersistingDefaultResorted() + public function testDefaultAndEntityTranslationWithoutPersistingDefaultResorted(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( false ); + $this->translatableListener->setPersistDefaultLocaleTranslation(false); $entity = new Article(); $this->repo ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ; $this->em->persist($entity); $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(1, $trans); - $this->assertSame('title translatedLocale', $trans['translatedLocale']['title']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(1, $trans); + static::assertSame('title translatedLocale', $trans['translatedLocale']['title']); - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title defaultLocale', $articles[0]['title']); + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title defaultLocale', $articles[0]['title']); } - public function testDefaultAndEntityTranslationWithPersistingDefault() + public function testDefaultAndEntityTranslationWithPersistingDefault(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( true ); + $this->translatableListener->setPersistDefaultLocaleTranslation(true); $entity = new Article(); $this->repo - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') ; @@ -307,73 +308,73 @@ public function testDefaultAndEntityTranslationWithPersistingDefault() $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(2, $trans); - $this->assertSame('title translatedLocale', $trans['translatedLocale']['title']); - $this->assertSame('title defaultLocale', $trans['defaultLocale']['title']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(2, $trans); + static::assertSame('title translatedLocale', $trans['translatedLocale']['title']); + static::assertSame('title defaultLocale', $trans['defaultLocale']['title']); - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title defaultLocale', $articles[0]['title']); + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title defaultLocale', $articles[0]['title']); } - public function testDefaultAndEntityTranslationWithPersistingDefaultResorted() + public function testDefaultAndEntityTranslationWithPersistingDefaultResorted(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( true ); + $this->translatableListener->setPersistDefaultLocaleTranslation(true); $entity = new Article(); $this->repo ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') - ->translate($entity, 'title', 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ; $this->em->persist($entity); $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(2, $trans); - $this->assertSame('title translatedLocale', $trans['translatedLocale']['title']); - $this->assertSame('title defaultLocale', $trans['defaultLocale']['title']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(2, $trans); + static::assertSame('title translatedLocale', $trans['translatedLocale']['title']); + static::assertSame('title defaultLocale', $trans['defaultLocale']['title']); - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title defaultLocale', $articles[0]['title']); + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title defaultLocale', $articles[0]['title']); } - public function testTwoFieldsWithoutPersistingDefault() + public function testTwoFieldsWithoutPersistingDefault(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( false ); + $this->translatableListener->setPersistDefaultLocaleTranslation(false); $entity = new Article(); $this->repo - ->translate($entity, 'title' , 'translatedLocale', 'title translatedLocale' ) - ->translate($entity, 'title' , 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ->translate($entity, 'content', 'translatedLocale', 'content translatedLocale') - ->translate($entity, 'content', 'defaultLocale' , 'content defaultLocale' ) + ->translate($entity, 'content', 'defaultLocale', 'content defaultLocale') ; $this->em->persist($entity); $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(1, $trans); - $this->assertSame('title translatedLocale' , $trans['translatedLocale']['title']); - $this->assertSame('content translatedLocale', $trans['translatedLocale']['content']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(1, $trans); + static::assertSame('title translatedLocale', $trans['translatedLocale']['title']); + static::assertSame('content translatedLocale', $trans['translatedLocale']['content']); - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title defaultLocale' , $articles[0]['title'] ); - $this->assertEquals('content defaultLocale', $articles[0]['content']); + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title defaultLocale', $articles[0]['title']); + static::assertSame('content defaultLocale', $articles[0]['content']); } - public function testTwoFieldsWithoutPersistingDefaultResorted() + public function testTwoFieldsWithoutPersistingDefaultResorted(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( false ); + $this->translatableListener->setPersistDefaultLocaleTranslation(false); $entity = new Article(); $this->repo - ->translate($entity, 'title' , 'defaultLocale' , 'title defaultLocale' ) - ->translate($entity, 'title' , 'translatedLocale', 'title translatedLocale' ) - ->translate($entity, 'content', 'defaultLocale' , 'content defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') + ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') + ->translate($entity, 'content', 'defaultLocale', 'content defaultLocale') ->translate($entity, 'content', 'translatedLocale', 'content translatedLocale') ; @@ -381,53 +382,53 @@ public function testTwoFieldsWithoutPersistingDefaultResorted() $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(1, $trans); - $this->assertSame('title translatedLocale' , $trans['translatedLocale']['title']); - $this->assertSame('content translatedLocale', $trans['translatedLocale']['content']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(1, $trans); + static::assertSame('title translatedLocale', $trans['translatedLocale']['title']); + static::assertSame('content translatedLocale', $trans['translatedLocale']['content']); - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title defaultLocale' , $articles[0]['title'] ); - $this->assertEquals('content defaultLocale', $articles[0]['content']); + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title defaultLocale', $articles[0]['title']); + static::assertSame('content defaultLocale', $articles[0]['content']); } - public function testTwoFieldsWithPersistingDefault() + public function testTwoFieldsWithPersistingDefault(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( true ); + $this->translatableListener->setPersistDefaultLocaleTranslation(true); $entity = new Article(); $this->repo - ->translate($entity, 'title' , 'translatedLocale', 'title translatedLocale' ) - ->translate($entity, 'title' , 'defaultLocale' , 'title defaultLocale' ) + ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') ->translate($entity, 'content', 'translatedLocale', 'content translatedLocale') - ->translate($entity, 'content', 'defaultLocale' , 'content defaultLocale' ) + ->translate($entity, 'content', 'defaultLocale', 'content defaultLocale') ; $this->em->persist($entity); $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(2, $trans); - $this->assertSame('title translatedLocale' , $trans['translatedLocale']['title']); - $this->assertSame('title defaultLocale' , $trans['defaultLocale']['title']); - $this->assertSame('content translatedLocale', $trans['translatedLocale']['content']); - $this->assertSame('content defaultLocale' , $trans['defaultLocale']['content']); - - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title defaultLocale' , $articles[0]['title'] ); - $this->assertEquals('content defaultLocale', $articles[0]['content']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(2, $trans); + static::assertSame('title translatedLocale', $trans['translatedLocale']['title']); + static::assertSame('title defaultLocale', $trans['defaultLocale']['title']); + static::assertSame('content translatedLocale', $trans['translatedLocale']['content']); + static::assertSame('content defaultLocale', $trans['defaultLocale']['content']); + + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title defaultLocale', $articles[0]['title']); + static::assertSame('content defaultLocale', $articles[0]['content']); } - public function testTwoFieldsWithPersistingDefaultResorted() + public function testTwoFieldsWithPersistingDefaultResorted(): void { - $this->translatableListener->setPersistDefaultLocaleTranslation( true ); + $this->translatableListener->setPersistDefaultLocaleTranslation(true); $entity = new Article(); $this->repo - ->translate($entity, 'title' , 'defaultLocale' , 'title defaultLocale' ) - ->translate($entity, 'title' , 'translatedLocale', 'title translatedLocale' ) - ->translate($entity, 'content', 'defaultLocale' , 'content defaultLocale' ) + ->translate($entity, 'title', 'defaultLocale', 'title defaultLocale') + ->translate($entity, 'title', 'translatedLocale', 'title translatedLocale') + ->translate($entity, 'content', 'defaultLocale', 'content defaultLocale') ->translate($entity, 'content', 'translatedLocale', 'content translatedLocale') ; @@ -435,27 +436,26 @@ public function testTwoFieldsWithPersistingDefaultResorted() $this->em->flush(); $this->em->clear(); - $trans = $this->repo->findTranslations($this->em->find(self::ARTICLE, $entity->getId())); - $this->assertCount(2, $trans); - $this->assertSame('title translatedLocale' , $trans['translatedLocale']['title']); - $this->assertSame('title defaultLocale' , $trans['defaultLocale']['title']); - $this->assertSame('content translatedLocale', $trans['translatedLocale']['content']); - $this->assertSame('content defaultLocale' , $trans['defaultLocale']['content']); - - $articles = $this->em->createQuery('SELECT a FROM '.self::ARTICLE.' a')->getArrayResult(); - $this->assertCount(1, $articles); - $this->assertEquals('title defaultLocale' , $articles[0]['title'] ); - $this->assertEquals('content defaultLocale', $articles[0]['content']); + $trans = $this->repo->findTranslations($this->em->find(Article::class, $entity->getId())); + static::assertCount(2, $trans); + static::assertSame('title translatedLocale', $trans['translatedLocale']['title']); + static::assertSame('title defaultLocale', $trans['defaultLocale']['title']); + static::assertSame('content translatedLocale', $trans['translatedLocale']['content']); + static::assertSame('content defaultLocale', $trans['defaultLocale']['content']); + + $articles = $this->em->createQuery('SELECT a FROM '.Article::class.' a')->getArrayResult(); + static::assertCount(1, $articles); + static::assertSame('title defaultLocale', $articles[0]['title']); + static::assertSame('content defaultLocale', $articles[0]['content']); } // --- Fixture related methods --------------------------------------------- - - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::TRANSLATION, - ); + return [ + Article::class, + Translation::class, + ]; } } diff --git a/tests/Gedmo/Translatable/TranslatableIdentifierTest.php b/tests/Gedmo/Translatable/TranslatableIdentifierTest.php index 86de570214..97c70ee5a5 100644 --- a/tests/Gedmo/Translatable/TranslatableIdentifierTest.php +++ b/tests/Gedmo/Translatable/TranslatableIdentifierTest.php @@ -1,27 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\StringIdentifier; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\StringIdentifier; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableIdentifierTest extends BaseTestCaseORM +final class TranslatableIdentifierTest extends BaseTestCaseORM { - const FIXTURE = 'Translatable\\Fixture\\StringIdentifier'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; + private ?string $testObjectId = null; - private $testObjectId; - private $translatableListener; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -31,38 +38,27 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en_us'); $evm->addEventSubscriber($this->translatableListener); - $conn = array( - 'driver' => 'pdo_mysql', - 'host' => '127.0.0.1', - 'dbname' => 'test', - 'user' => 'root', - 'password' => 'nimda', - ); - //$this->getMockCustomEntityManager($conn, $evm); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldHandleStringIdentifier() + public function testShouldHandleStringIdentifier(): void { $object = new StringIdentifier(); $object->setTitle('title in en'); - $object->setUid(md5(self::FIXTURE.time())); + $object->setUid(md5(StringIdentifier::class.time())); $this->em->persist($object); $this->em->flush(); $this->em->clear(); $this->testObjectId = $object->getUid(); - $repo = $this->em->getRepository(self::TRANSLATION); - $object = $this->em->find(self::FIXTURE, $this->testObjectId); + $repo = $this->em->getRepository(Translation::class); + $object = $this->em->find(StringIdentifier::class, $this->testObjectId); $translations = $repo->findTranslations($object); - $this->assertCount(0, $translations); + static::assertCount(0, $translations); - $object = $this->em->find(self::FIXTURE, $this->testObjectId); + $object = $this->em->find(StringIdentifier::class, $this->testObjectId); $object->setTitle('title in de'); $object->setTranslatableLocale('de_de'); @@ -70,48 +66,48 @@ public function shouldHandleStringIdentifier() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); // test the entity load by translated title $object = $repo->findObjectByTranslatedField( 'title', 'title in de', - self::FIXTURE + StringIdentifier::class ); - $this->assertEquals($this->testObjectId, $object->getUid()); + static::assertSame($this->testObjectId, $object->getUid()); $translations = $repo->findTranslations($object); - $this->assertCount(1, $translations); - $this->assertArrayHasKey('de_de', $translations); + static::assertCount(1, $translations); + static::assertArrayHasKey('de_de', $translations); - $this->assertArrayHasKey('title', $translations['de_de']); - $this->assertEquals('title in de', $translations['de_de']['title']); + static::assertArrayHasKey('title', $translations['de_de']); + static::assertSame('title in de', $translations['de_de']['title']); // dql test object hydration $q = $this->em - ->createQuery('SELECT si FROM '.self::FIXTURE.' si WHERE si.uid = :id') + ->createQuery('SELECT si FROM '.StringIdentifier::class.' si WHERE si.uid = :id') ->setParameter('id', $this->testObjectId) - ->useResultCache(false) + ->disableResultCache() ; $data = $q->getResult(); - $this->assertCount(1, $data); + static::assertCount(1, $data); $object = $data[0]; - $this->assertEquals('title in en', $object->getTitle()); + static::assertSame('title in en', $object->getTitle()); $this->em->clear(); // based on 2.3.0 it caches in identity map $this->translatableListener->setTranslatableLocale('de_de'); $data = $q->getResult(); - $this->assertCount(1, $data); + static::assertCount(1, $data); $object = $data[0]; - $this->assertEquals('title in de', $object->getTitle()); + static::assertSame('title in de', $object->getTitle()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::FIXTURE, - self::TRANSLATION, - ); + return [ + StringIdentifier::class, + Translation::class, + ]; } } diff --git a/tests/Gedmo/Translatable/TranslatableTest.php b/tests/Gedmo/Translatable/TranslatableTest.php index 11ca6b51bd..dc3b1d5a17 100644 --- a/tests/Gedmo/Translatable/TranslatableTest.php +++ b/tests/Gedmo/Translatable/TranslatableTest.php @@ -1,31 +1,38 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\Article; -use Translatable\Fixture\Comment; -use Translatable\Fixture\Sport; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Article; +use Gedmo\Tests\Translatable\Fixture\Comment; +use Gedmo\Tests\Translatable\Fixture\Sport; +use Gedmo\Translatable\Entity\Repository\TranslationRepository; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Translatable; +use Gedmo\Translatable\TranslatableListener; /** * These are tests for translatable behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableTest extends BaseTestCaseORM +final class TranslatableTest extends BaseTestCaseORM { - const ARTICLE = 'Translatable\\Fixture\\Article'; - const SPORT = 'Translatable\\Fixture\\Sport'; - const COMMENT = 'Translatable\\Fixture\\Comment'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; + private ?int $articleId = null; - private $articleId; - private $translatableListener; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -35,17 +42,14 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en_us'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldUpdateTranslationInDefaultLocaleIssue751() + public function testShouldUpdateTranslationInDefaultLocaleIssue751(): void { $this->translatableListener->setTranslatableLocale('en'); $this->translatableListener->setDefaultLocale('en'); - $repo = $this->em->getRepository(self::ARTICLE); + $repo = $this->em->getRepository(Article::class); $entity = new Article(); $entity->setTranslatableLocale('de'); @@ -62,26 +66,23 @@ public function shouldUpdateTranslationInDefaultLocaleIssue751() // this will force it to find translation in "en" locale, since listener has "en" set // and since default locale is "en" current translation will be "test" // setting title to "test" will not even persist entity, since there is no changeset - $entity = $repo->findOneById($entity->getId()); + $entity = $repo->findOneBy(['id' => $entity->getId()]); $entity->setTranslatableLocale('de'); $entity->setTitle('test'); $this->em->persist($entity); $this->em->flush(); $this->em->clear(); - $entity = $repo->findOneById($entity->getId()); - $repo = $this->em->getRepository(self::TRANSLATION); + $entity = $repo->findOneBy(['id' => $entity->getId()]); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($entity); - $this->assertArrayHasKey('de', $translations); - $this->assertSame('test!', $translations['de']['title']); // de translation was not updated, no changeset - $this->assertSame('test', $entity->getTitle()); // obviously "test" a default en translation + static::assertArrayHasKey('de', $translations); + static::assertSame('test!', $translations['de']['title']); // de translation was not updated, no changeset + static::assertSame('test', $entity->getTitle()); // obviously "test" a default en translation } - /** - * @test - */ - public function shouldPersistDefaultLocaleTranslationIfRequired() + public function testShouldPersistDefaultLocaleTranslationIfRequired(): void { $this->translatableListener->setPersistDefaultLocaleTranslation(true); @@ -92,37 +93,34 @@ public function shouldPersistDefaultLocaleTranslationIfRequired() $this->em->persist($article); $this->em->flush(); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($article); - $this->assertCount(1, $translations); - $this->assertArrayHasKey('en_us', $translations); + static::assertCount(1, $translations); + static::assertArrayHasKey('en_us', $translations); } - /** - * @test - */ - public function shouldGenerateTranslations() + public function testShouldGenerateTranslations(): void { $this->populate(); - $repo = $this->em->getRepository(self::TRANSLATION); - $this->assertTrue($repo instanceof Entity\Repository\TranslationRepository); + $repo = $this->em->getRepository(Translation::class); + static::assertInstanceOf(TranslationRepository::class, $repo); - $article = $this->em->find(self::ARTICLE, $this->articleId); - $this->assertTrue($article instanceof Translatable); + $article = $this->em->find(Article::class, $this->articleId); + static::assertInstanceOf(Translatable::class, $article); $translations = $repo->findTranslations($article); - $this->assertCount(0, $translations); + static::assertCount(0, $translations); $comments = $article->getComments(); - $this->assertCount(2, $comments); - foreach ($comments as $num => $comment) { + static::assertCount(2, $comments); + foreach ($comments as $comment) { $translations = $repo->findTranslations($comment); - $this->assertCount(0, $translations); + static::assertCount(0, $translations); } // test default locale - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(Article::class, $this->articleId); $article->setTranslatableLocale('de_de'); $article->setContent('content in de'); $article->setTitle('title in de'); @@ -133,30 +131,28 @@ public function shouldGenerateTranslations() $qb = $this->em->createQueryBuilder(); $qb->select('art') - ->from(self::ARTICLE, 'art') - ->where('art.id = :id'); + ->from(Article::class, 'art') + ->where('art.id = :id') + ->setParameter('id', $article->getId()); $q = $qb->getQuery(); - $result = $q->execute( - array('id' => $article->getId()), - \Doctrine\ORM\Query::HYDRATE_ARRAY - ); - $this->assertCount(1, $result); - $this->assertEquals('title in en', $result[0]['title']); - $this->assertEquals('content in en', $result[0]['content']); - - $repo = $this->em->getRepository(self::TRANSLATION); + $result = $q->getArrayResult(); + static::assertCount(1, $result); + static::assertSame('title in en', $result[0]['title']); + static::assertSame('content in en', $result[0]['content']); + + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($article); - $this->assertCount(1, $translations); - $this->assertArrayHasKey('de_de', $translations); + static::assertCount(1, $translations); + static::assertArrayHasKey('de_de', $translations); - $this->assertArrayHasKey('content', $translations['de_de']); - $this->assertEquals('content in de', $translations['de_de']['content']); + static::assertArrayHasKey('content', $translations['de_de']); + static::assertSame('content in de', $translations['de_de']['content']); - $this->assertArrayHasKey('title', $translations['de_de']); - $this->assertEquals('title in de', $translations['de_de']['title']); + static::assertArrayHasKey('title', $translations['de_de']); + static::assertSame('title in de', $translations['de_de']['title']); // test second translations - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(Article::class, $this->articleId); $article->setTranslatableLocale('de_de'); $article->setContent('content in de'); $article->setTitle('title in de'); @@ -173,84 +169,78 @@ public function shouldGenerateTranslations() $this->em->flush(); $this->em->clear(); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($article); - $this->assertCount(1, $translations); - $this->assertArrayHasKey('de_de', $translations); + static::assertCount(1, $translations); + static::assertArrayHasKey('de_de', $translations); - $this->assertArrayHasKey('content', $translations['de_de']); - $this->assertEquals('content in de', $translations['de_de']['content']); + static::assertArrayHasKey('content', $translations['de_de']); + static::assertSame('content in de', $translations['de_de']['content']); - $this->assertArrayHasKey('title', $translations['de_de']); - $this->assertEquals('title in de', $translations['de_de']['title']); + static::assertArrayHasKey('title', $translations['de_de']); + static::assertSame('title in de', $translations['de_de']['title']); $comments = $article->getComments(); - $this->assertCount(2, $comments); + static::assertCount(2, $comments); foreach ($comments as $comment) { $translations = $repo->findTranslations($comment); - $this->assertCount(1, $translations); - $this->assertArrayHasKey('de_de', $translations); + static::assertCount(1, $translations); + static::assertArrayHasKey('de_de', $translations); $number = preg_replace("@[^\d]+@", '', $comment->getSubject()); - $this->assertArrayHasKey('subject', $translations['de_de']); + static::assertArrayHasKey('subject', $translations['de_de']); $expected = "subject{$number} in de"; - $this->assertEquals($expected, $translations['de_de']['subject']); + static::assertSame($expected, $translations['de_de']['subject']); - $this->assertArrayHasKey('message', $translations['de_de']); + static::assertArrayHasKey('message', $translations['de_de']); $expected = "message{$number} in de"; - $this->assertEquals($expected, $translations['de_de']['message']); + static::assertSame($expected, $translations['de_de']['message']); } - $article = $this->em->find(self::ARTICLE, $this->articleId); - $this->assertEquals('title in en', $article->getTitle()); - $this->assertEquals('content in en', $article->getContent()); + $article = $this->em->find(Article::class, $this->articleId); + static::assertSame('title in en', $article->getTitle()); + static::assertSame('content in en', $article->getContent()); $comments = $article->getComments(); foreach ($comments as $comment) { $number = preg_replace("@[^\d]+@", '', $comment->getSubject()); - $this->assertEquals("subject{$number} in en", $comment->getSubject()); - $this->assertEquals("message{$number} in en", $comment->getMessage()); + static::assertSame("subject{$number} in en", $comment->getSubject()); + static::assertSame("message{$number} in en", $comment->getMessage()); } // test deletion - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(Article::class, $this->articleId); $this->em->remove($article); $this->em->flush(); $translations = $repo->findTranslations($article); - $this->assertCount(0, $translations); + static::assertCount(0, $translations); } - /** - * @test - */ - public function shouldSolveTranslationFallbackGithubIssue9() + public function testShouldSolveTranslationFallbackGithubIssue9(): void { $this->populate(); $this->translatableListener->setTranslationFallback(false); $this->translatableListener->setTranslatableLocale('ru_RU'); - $article = $this->em->find(self::ARTICLE, $this->articleId); - $this->assertFalse((bool) $article->getTitle()); - $this->assertFalse((bool) $article->getContent()); + $article = $this->em->find(Article::class, $this->articleId); + static::assertFalse((bool) $article->getTitle()); + static::assertFalse((bool) $article->getContent()); foreach ($article->getComments() as $comment) { - $this->assertFalse((bool) $comment->getSubject()); - $this->assertFalse((bool) $comment->getMessage()); + static::assertFalse((bool) $comment->getSubject()); + static::assertFalse((bool) $comment->getMessage()); } $this->em->clear(); $this->translatableListener->setTranslationFallback(true); - $article = $this->em->find(self::ARTICLE, $this->articleId); + $article = $this->em->find(Article::class, $this->articleId); - $this->assertEquals('title in en', $article->getTitle()); - $this->assertEquals('content in en', $article->getContent()); + static::assertSame('title in en', $article->getTitle()); + static::assertSame('content in en', $article->getContent()); } - /** - * @test - */ - public function shouldSolveGithubIssue64() + public function testShouldSolveGithubIssue64(): void { $judo = new Sport(); $judo->setTitle('Judo'); @@ -266,9 +256,9 @@ public function shouldSolveGithubIssue64() $this->em->persist($judo); $this->em->flush(); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($judo); - $this->assertCount(1, $translations); + static::assertCount(1, $translations); // now without any changeset $this->translatableListener->setTranslatableLocale('ru_ru'); @@ -280,13 +270,10 @@ public function shouldSolveGithubIssue64() // this will not add additional translation, because it cannot be tracked // without anything in changeset $translations = $repo->findTranslations($judo); - $this->assertCount(1, $translations); + static::assertCount(1, $translations); } - /** - * @test - */ - public function shouldRespectFallbackOption() + public function testShouldRespectFallbackOption(): void { $article = new Article(); $article->setTitle('Euro2012'); @@ -299,31 +286,31 @@ public function shouldRespectFallbackOption() $this->translatableListener->setTranslatableLocale('ua_UA'); $this->translatableListener->setTranslationFallback(true); - $article = $this->em->find(self::ARTICLE, $article->getId()); + $article = $this->em->find(Article::class, $article->getId()); - $this->assertEquals('Euro2012', $article->getTitle()); - $this->assertEquals('Shevchenko', $article->getAuthor()); - $this->assertEmpty($article->getViews()); + static::assertSame('Euro2012', $article->getTitle()); + static::assertSame('Shevchenko', $article->getAuthor()); + static::assertEmpty($article->getViews()); $this->em->clear(); $this->translatableListener->setTranslationFallback(false); - $article = $this->em->find(self::ARTICLE, $article->getId()); - $this->assertEmpty($article->getTitle()); - $this->assertEquals('Shevchenko', $article->getAuthor()); - $this->assertEmpty($article->getViews()); + $article = $this->em->find(Article::class, $article->getId()); + static::assertEmpty($article->getTitle()); + static::assertSame('Shevchenko', $article->getAuthor()); + static::assertEmpty($article->getViews()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::TRANSLATION, - self::COMMENT, - self::SPORT, - ); + return [ + Article::class, + Translation::class, + Comment::class, + Sport::class, + ]; } - private function populate() + private function populate(): void { $article = new Article(); $article->setTitle('title in en'); diff --git a/tests/Gedmo/Translatable/TranslatableWithEmbeddedTest.php b/tests/Gedmo/Translatable/TranslatableWithEmbeddedTest.php index a4598341f5..76298e6999 100644 --- a/tests/Gedmo/Translatable/TranslatableWithEmbeddedTest.php +++ b/tests/Gedmo/Translatable/TranslatableWithEmbeddedTest.php @@ -1,26 +1,30 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; use Doctrine\Common\EventManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query; -use Tool\BaseTestCaseORM; -use Translatable\Fixture\Company; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Company; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; +use Gedmo\Translatable\TranslatableListener; -class TranslatableWithEmbeddedTest extends BaseTestCaseORM +final class TranslatableWithEmbeddedTest extends BaseTestCaseORM { - const FIXTURE = 'Translatable\\Fixture\\Company'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; - - const TREE_WALKER_TRANSLATION = 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'; - - /** - * @var TranslatableListener - */ - private $translatableListener; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -30,11 +34,11 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en_us'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function populate() + public function populate(): void { $entity = new Company(); $entity->setTitle('test'); @@ -54,66 +58,60 @@ public function populate() $this->em->clear(); } - /** - * @test - */ - public function testTranslate() + public function testTranslate(): void { - /** @var EntityRepository $repo */ - $repo = $this->em->getRepository(self::FIXTURE); + /** @var EntityRepository $repo */ + $repo = $this->em->getRepository(Company::class); /** @var Company $entity */ - $entity = $repo->findOneById(1); + $entity = $repo->findOneBy(['id' => 1]); - $repo = $this->em->getRepository(self::TRANSLATION); + $repo = $this->em->getRepository(Translation::class); $translations = $repo->findTranslations($entity); - $this->assertArrayHasKey('de', $translations); - $this->assertSame('test-de', $translations['de']['title']); - $this->assertSame('test', $entity->getTitle()); + static::assertArrayHasKey('de', $translations); + static::assertSame('test-de', $translations['de']['title']); + static::assertSame('test', $entity->getTitle()); - $this->assertSame('website-de', $translations['de']['link.website']); - $this->assertSame('website', $entity->getLink()->getWebsite()); + static::assertSame('website-de', $translations['de']['link.website']); + static::assertSame('website', $entity->getLink()->getWebsite()); - $this->assertSame('facebook-de', $translations['de']['link.facebook']); - $this->assertSame('facebook', $entity->getLink()->getFacebook()); + static::assertSame('facebook-de', $translations['de']['link.facebook']); + static::assertSame('facebook', $entity->getLink()->getFacebook()); $this->em->clear(); $this->translatableListener->setTranslatableLocale('de'); - $repo = $this->em->getRepository(self::FIXTURE); - $entity = $repo->findOneById($entity->getId()); + $repo = $this->em->getRepository(Company::class); + $entity = $repo->findOneBy(['id' => $entity->getId()]); - $this->assertSame('website-de', $entity->getLink()->getWebsite()); - $this->assertSame('facebook-de', $entity->getLink()->getFacebook()); + static::assertSame('website-de', $entity->getLink()->getWebsite()); + static::assertSame('facebook-de', $entity->getLink()->getFacebook()); } - /** - * @test - */ - public function testQueryWalker() + public function testQueryWalker(): void { - $dql = 'SELECT f FROM '.self::FIXTURE.' f'; + $dql = 'SELECT f FROM '.Company::class.' f'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $this->translatableListener->setTranslatableLocale('de'); $result = $q->getArrayResult(); - $this->assertCount(1, $result); - $this->assertSame('test-de', $result[0]['title']); - $this->assertSame('website-de', $result[0]['link.website']); - $this->assertSame('facebook-de', $result[0]['link.facebook']); + static::assertCount(1, $result); + static::assertSame('test-de', $result[0]['title']); + static::assertSame('website-de', $result[0]['link.website']); + static::assertSame('facebook-de', $result[0]['link.facebook']); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::FIXTURE, - self::TRANSLATION, - ); + return [ + Company::class, + Translation::class, + ]; } -} \ No newline at end of file +} diff --git a/tests/Gedmo/Translatable/TranslationQueryWalkerTest.php b/tests/Gedmo/Translatable/TranslationQueryWalkerTest.php index d8ee9e00f6..7e42c8207b 100644 --- a/tests/Gedmo/Translatable/TranslationQueryWalkerTest.php +++ b/tests/Gedmo/Translatable/TranslationQueryWalkerTest.php @@ -1,35 +1,38 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translatable; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; use Doctrine\ORM\Query; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translatable\Fixture\Article; +use Gedmo\Tests\Translatable\Fixture\Comment; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\Hydrator\ORM\ObjectHydrator; +use Gedmo\Translatable\Hydrator\ORM\SimpleObjectHydrator; use Gedmo\Translatable\Query\TreeWalker\TranslationWalker; -use Translatable\Fixture\Article; -use Translatable\Fixture\Comment; +use Gedmo\Translatable\TranslatableListener; +use Symfony\Component\Cache\Adapter\ArrayAdapter; /** * These are tests for translation query walker * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslationQueryWalkerTest extends BaseTestCaseORM +final class TranslationQueryWalkerTest extends BaseTestCaseORM { - const ARTICLE = 'Translatable\\Fixture\\Article'; - const COMMENT = 'Translatable\\Fixture\\Comment'; - const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation'; + private TranslatableListener $translatableListener; - const TREE_WALKER_TRANSLATION = 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'; - - /** - * @var TranslatableListener - */ - private $translatableListener; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -39,93 +42,75 @@ protected function setUp() $this->translatableListener->setDefaultLocale('en_us'); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - /** - * @test - */ - function shouldHandleQueryCache() + public function testShouldHandleQueryCache(): void { - $cache = new \Doctrine\Common\Cache\ArrayCache(); - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getQueryCacheImpl') - ->will($this->returnValue($cache)) - ; - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $this->em->getConfiguration()->setQueryCache(new ArrayAdapter()); + $dql = 'SELECT a FROM '.Article::class.' a'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getArrayResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $q2 = clone $q; - $q2->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q2->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $result = $q->getArrayResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); } - /** - * @test - */ - function subselectByTranslatedField() + public function testSubselectByTranslatedField(): void { $this->populateMore(); - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; - $subSelect = 'SELECT a2.title FROM '.self::ARTICLE.' a2'; + $dql = 'SELECT a FROM '.Article::class.' a'; + $subSelect = 'SELECT a2.title FROM '.Article::class.' a2'; $subSelect .= " WHERE a2.title LIKE '%ab%'"; $dql .= " WHERE a.title IN ({$subSelect})"; $dql .= ' ORDER BY a.title'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getArrayResult(); - $this->assertCount(2, $result); - $this->assertEquals('Alfabet', $result[0]['title']); - $this->assertEquals('Cabbages', $result[1]['title']); + static::assertCount(2, $result); + static::assertSame('Alfabet', $result[0]['title']); + static::assertSame('Cabbages', $result[1]['title']); } - /** - * @test - */ - function subselectStatements() + public function testSubselectStatements(): void { $this->populateMore(); - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; - $subSelect = 'SELECT a2.id FROM '.self::ARTICLE.' a2'; + $dql = 'SELECT a FROM '.Article::class.' a'; + $subSelect = 'SELECT a2.id FROM '.Article::class.' a2'; $subSelect .= " WHERE a2.title LIKE '%ab%'"; $dql .= " WHERE a.id IN ({$subSelect})"; $dql .= ' ORDER BY a.title'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getArrayResult(); - $this->assertCount(2, $result); - $this->assertEquals('Alfabet', $result[0]['title']); - $this->assertEquals('Cabbages', $result[1]['title']); + static::assertCount(2, $result); + static::assertSame('Alfabet', $result[0]['title']); + static::assertSame('Cabbages', $result[1]['title']); } - /** - * @test - */ - function joinedWithStatements() + public function testJoinedWithStatements(): void { $this->populateMore(); - $dql = 'SELECT a, c FROM '.self::ARTICLE.' a'; + $dql = 'SELECT a, c FROM '.Article::class.' a'; $dql .= ' LEFT JOIN a.comments c WITH c.subject LIKE :lookup'; $dql .= ' WHERE a.title LIKE :filter'; $dql .= ' ORDER BY a.title'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); @@ -133,122 +118,138 @@ function joinedWithStatements() $q->setParameter('filter', 'Foo%'); $result = $q->getArrayResult(); - $this->assertCount(1, $result); - $this->assertEquals('Food', $result[0]['title']); + static::assertCount(1, $result); + static::assertSame('Food', $result[0]['title']); $comments = $result[0]['comments']; - $this->assertCount(1, $comments); - $this->assertEquals('good', $comments[0]['subject']); + static::assertCount(1, $comments); + static::assertSame('good', $comments[0]['subject']); } /** - * @test + * @doesNotPerformAssertions */ - function shouldSelectWithTranslationFallbackOnSimpleObjectHydration() + public function testPaginatedQuery(): void + { + $this->populateMore(); + + $dql = 'SELECT a FROM '.Article::class.' a'; + $q = $this->em->createQuery($dql); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); + $q->setFirstResult(0); + $q->setMaxResults(1); + $q->getResult(Query::HYDRATE_SIMPLEOBJECT); + } + + public function testShouldSelectWithTranslationFallbackOnSimpleObjectHydration(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_SIMPLE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\SimpleObjectHydrator')); - - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_SIMPLE_OBJECT_TRANSLATION, + SimpleObjectHydrator::class + ); + + $dql = 'SELECT a FROM '.Article::class.' a'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $this->translatableListener->setTranslatableLocale('ru_ru'); $this->translatableListener->setTranslationFallback(false); + $this->queryLogger->reset(); + // simple object hydration - $this->startQueryLog(); $result = $q->getResult(Query::HYDRATE_SIMPLEOBJECT); - $this->assertEquals(1, $this->queryAnalyzer->getNumExecutedQueries()); - $this->assertEquals('', $result[0]->getTitle()); - $this->assertEquals('', $result[0]->getContent()); + + static::assertCount(1, $this->queryLogger->queries); + $this->queryLogger->reset(); + + static::assertNull($result[0]->getTitle()); + static::assertNull($result[0]->getContent()); $this->translatableListener->setTranslationFallback(true); - $this->queryAnalyzer->cleanUp(); + $result = $q->getResult(Query::HYDRATE_SIMPLEOBJECT); - $this->assertEquals(1, $this->queryAnalyzer->getNumExecutedQueries()); - //Default translation is en_us, so we expect the results in that locale - $this->assertEquals('Food', $result[0]->getTitle()); - $this->assertEquals('about food', $result[0]->getContent()); + + static::assertCount(1, $this->queryLogger->queries); + + // Default translation is en_us, so we expect the results in that locale + static::assertSame('Food', $result[0]->getTitle()); + static::assertSame('about food', $result[0]->getContent()); } - /** - * @test - */ - function selectWithTranslationFallbackOnArrayHydration() + public function testSelectWithTranslationFallbackOnArrayHydration(): void { - $dql = 'SELECT a, c FROM '.self::ARTICLE.' a'; + $dql = 'SELECT a, c FROM '.Article::class.' a'; $dql .= ' LEFT JOIN a.comments c'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $this->translatableListener->setTranslatableLocale('ru_ru'); $this->translatableListener->setTranslationFallback(false); + $this->queryLogger->reset(); + // array hydration - $this->startQueryLog(); $result = $q->getArrayResult(); - $this->assertEquals(1, $this->queryAnalyzer->getNumExecutedQueries()); - $this->assertEquals('', $result[0]['title']); - $this->assertEquals('', $result[0]['content']); + + static::assertCount(1, $this->queryLogger->queries); + $this->queryLogger->reset(); + + static::assertNull($result[0]['title']); + static::assertNull($result[0]['content']); $this->translatableListener->setTranslationFallback(true); - $this->queryAnalyzer->cleanUp(); + $result = $q->getArrayResult(); - $this->assertEquals(1, $this->queryAnalyzer->getNumExecutedQueries()); - //Default translation is en_us, so we expect the results in that locale - $this->assertEquals('Food', $result[0]['title']); - $this->assertEquals('about food', $result[0]['content']); + + static::assertCount(1, $this->queryLogger->queries); + + // Default translation is en_us, so we expect the results in that locale + static::assertSame('Food', $result[0]['title']); + static::assertSame('about food', $result[0]['content']); } - /** - * @test - */ - function selectWithOptionalFallbackOnSimpleObjectHydration() + public function testSelectWithOptionalFallbackOnSimpleObjectHydration(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_SIMPLE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\SimpleObjectHydrator')); - - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_SIMPLE_OBJECT_TRANSLATION, + SimpleObjectHydrator::class + ); + + $dql = 'SELECT a FROM '.Article::class.' a'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $this->translatableListener->setTranslatableLocale('ru_ru'); $this->translatableListener->setTranslationFallback(false); + $this->queryLogger->reset(); + // simple object hydration - $this->startQueryLog(); $result = $q->getResult(Query::HYDRATE_SIMPLEOBJECT); - $this->assertEquals(1, $this->queryAnalyzer->getNumExecutedQueries()); - $this->assertEquals('', $result[0]->getTitle()); - $this->assertEquals('John Doe', $result[0]->getAuthor()); // optional fallback is true, force fallback - $this->assertEquals(0, $result[0]->getViews()); + + static::assertCount(1, $this->queryLogger->queries); + $this->queryLogger->reset(); + + static::assertNull($result[0]->getTitle()); + static::assertSame('John Doe', $result[0]->getAuthor()); // optional fallback is true, force fallback + static::assertNull($result[0]->getViews()); $this->translatableListener->setTranslationFallback(true); - $this->queryAnalyzer->cleanUp(); $result = $q->getResult(Query::HYDRATE_SIMPLEOBJECT); - $this->assertEquals(1, $this->queryAnalyzer->getNumExecutedQueries()); - //Default translation is en_us, so we expect the results in that locale - $this->assertEquals('Food', $result[0]->getTitle()); - $this->assertEquals('John Doe', $result[0]->getAuthor()); - $this->assertEquals(0, $result[0]->getViews()); // optional fallback is false, thus no translation required + + static::assertCount(1, $this->queryLogger->queries); + + // Default translation is en_us, so we expect the results in that locale + static::assertSame('Food', $result[0]->getTitle()); + static::assertSame('John Doe', $result[0]->getAuthor()); + static::assertNull($result[0]->getViews()); // optional fallback is false, thus no translation required } - /** - * @test - */ - function shouldBeAbleToUseInnerJoinStrategyForTranslations() + public function testShouldBeAbleToUseInnerJoinStrategyForTranslations(): void { - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $dql = 'SELECT a FROM '.Article::class.' a'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $q->setHint(TranslatableListener::HINT_INNER_JOIN, true); $this->translatableListener->setTranslatableLocale('ru_ru'); @@ -256,46 +257,42 @@ function shouldBeAbleToUseInnerJoinStrategyForTranslations() // array hydration $result = $q->getArrayResult(); - $this->assertCount(0, $result); + static::assertCount(0, $result); } /** * referres to issue #755 - * @test */ - function shouldBeAbleToOverrideTranslationFallbackByHint() + public function testShouldBeAbleToOverrideTranslationFallbackByHint(): void { $this->translatableListener->setTranslatableLocale('lt_lt'); $this->translatableListener->setTranslationFallback(false); - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $dql = 'SELECT a FROM '.Article::class.' a'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $q->setHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'undefined'); $q->setHint(TranslatableListener::HINT_FALLBACK, true); // array hydration $result = $q->getArrayResult(); - $this->assertCount(1, $result); - $this->assertEquals('Food', $result[0]['title']); + static::assertCount(1, $result); + static::assertSame('Food', $result[0]['title']); // fallback false hint $q->setHint(TranslatableListener::HINT_FALLBACK, false); // array hydration $result = $q->getArrayResult(); - $this->assertCount(1, $result); - $this->assertEquals(null, $result[0]['title']); + static::assertCount(1, $result); + static::assertNull($result[0]['title']); } - /** - * @test - */ - function shouldBeAbleToOverrideTranslatableLocale() + public function testShouldBeAbleToOverrideTranslatableLocale(): void { - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $dql = 'SELECT a FROM '.Article::class.' a'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $q->setHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'lt_lt'); $this->translatableListener->setTranslatableLocale('ru_ru'); @@ -303,440 +300,402 @@ function shouldBeAbleToOverrideTranslatableLocale() // array hydration $result = $q->getArrayResult(); - $this->assertCount(1, $result); - $this->assertEquals('Maistas', $result[0]['title']); + static::assertCount(1, $result); + static::assertSame('Maistas', $result[0]['title']); } - /** - * @test - */ - function shouldSelectWithTranslationFallbackOnObjectHydration() + public function testShouldSelectWithTranslationFallbackOnObjectHydration(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator')); - - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_OBJECT_TRANSLATION, + ObjectHydrator::class + ); + + $dql = 'SELECT a FROM '.Article::class.' a'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $this->translatableListener->setTranslatableLocale('ru_ru'); $this->translatableListener->setTranslationFallback(false); + $this->queryLogger->reset(); + // object hydration - $this->startQueryLog(); $result = $q->getResult(); - $this->assertEquals(1, $this->queryAnalyzer->getNumExecutedQueries()); - $this->assertEquals('', $result[0]->getTitle()); - $this->assertEquals('', $result[0]->getContent()); + + static::assertCount(1, $this->queryLogger->queries); + $this->queryLogger->reset(); + + static::assertNull($result[0]->getTitle()); + static::assertNull($result[0]->getContent()); $this->translatableListener->setTranslationFallback(true); - $this->queryAnalyzer->cleanUp(); $result = $q->getResult(); - $this->assertEquals(1, $this->queryAnalyzer->getNumExecutedQueries()); - //Default translation is en_us, so we expect the results in that locale - $this->assertEquals('Food', $result[0]->getTitle()); - $this->assertEquals('about food', $result[0]->getContent()); + + static::assertCount(1, $this->queryLogger->queries); + + // Default translation is en_us, so we expect the results in that locale + static::assertSame('Food', $result[0]->getTitle()); + static::assertSame('about food', $result[0]->getContent()); // test fallback hint $this->translatableListener->setTranslationFallback(false); $q->setHint(TranslatableListener::HINT_FALLBACK, 1); $result = $q->getResult(); - //Default translation is en_us, so we expect the results in that locale - $this->assertEquals('Food', $result[0]->getTitle()); - $this->assertEquals('about food', $result[0]->getContent()); + // Default translation is en_us, so we expect the results in that locale + static::assertSame('Food', $result[0]->getTitle()); + static::assertSame('about food', $result[0]->getContent()); // test fallback hint $this->translatableListener->setTranslationFallback(true); $q->setHint(TranslatableListener::HINT_FALLBACK, 0); $result = $q->getResult(); - //Default translation is en_us, so we expect the results in that locale - $this->assertEquals('', $result[0]->getTitle()); - $this->assertEquals('', $result[0]->getContent()); + // Default translation is en_us, so we expect the results in that locale + static::assertNull($result[0]->getTitle()); + static::assertNull($result[0]->getContent()); } - /** - * @test - */ - function shouldSelectCountStatement() + public function testShouldSelectCountStatement(): void { - $dql = 'SELECT COUNT(a) FROM '.self::ARTICLE.' a'; + $dql = 'SELECT COUNT(a) FROM '.Article::class.' a'; $dql .= ' WHERE a.title LIKE :title'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); $this->translatableListener->setTranslatableLocale('en_us'); $q->setParameter('title', 'Foo%'); $result = $q->getSingleScalarResult(); - $this->assertEquals(1, $result); + static::assertSame(1, (int) $result); $this->translatableListener->setTranslatableLocale('lt_lt'); $q->setParameter('title', 'Mai%'); $result = $q->getSingleScalarResult(); - $this->assertEquals(1, $result); + static::assertSame(1, (int) $result); $this->translatableListener->setTranslatableLocale('en_us'); $q->setParameter('title', 'Mai%'); $result = $q->getSingleScalarResult(); - $this->assertEquals(0, $result); + static::assertSame(0, (int) $result); } - /** - * @test - */ - function shouldSelectOrderedJoinedComponentTranslation() + public function testShouldSelectOrderedJoinedComponentTranslation(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator')); + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_OBJECT_TRANSLATION, + ObjectHydrator::class + ); $this->populateMore(); - $dql = 'SELECT a, c FROM '.self::ARTICLE.' a'; + $dql = 'SELECT a, c FROM '.Article::class.' a'; $dql .= ' LEFT JOIN a.comments c'; $dql .= ' ORDER BY a.title'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getArrayResult(); - $this->assertCount(4, $result); - $this->assertEquals('Alfabet', $result[0]['title']); - $this->assertEquals('Cabbages', $result[1]['title']); - $this->assertEquals('Food', $result[2]['title']); - $this->assertEquals('Woman', $result[3]['title']); + static::assertCount(4, $result); + static::assertSame('Alfabet', $result[0]['title']); + static::assertSame('Cabbages', $result[1]['title']); + static::assertSame('Food', $result[2]['title']); + static::assertSame('Woman', $result[3]['title']); $this->translatableListener->setTranslatableLocale('lt_lt'); $result = $q->getArrayResult(); - $this->assertCount(4, $result); - $this->assertEquals('Alfabetas', $result[0]['title']); - $this->assertEquals('Kopustai', $result[1]['title']); - $this->assertEquals('Maistas', $result[2]['title']); - $this->assertEquals('Moteris', $result[3]['title']); + static::assertCount(4, $result); + static::assertSame('Alfabetas', $result[0]['title']); + static::assertSame('Kopustai', $result[1]['title']); + static::assertSame('Maistas', $result[2]['title']); + static::assertSame('Moteris', $result[3]['title']); // object hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getResult(); - $this->assertCount(4, $result); - $this->assertEquals('Alfabet', $result[0]->getTitle()); - $this->assertEquals('Cabbages', $result[1]->getTitle()); - $this->assertEquals('Food', $result[2]->getTitle()); - $this->assertEquals('Woman', $result[3]->getTitle()); + static::assertCount(4, $result); + static::assertSame('Alfabet', $result[0]->getTitle()); + static::assertSame('Cabbages', $result[1]->getTitle()); + static::assertSame('Food', $result[2]->getTitle()); + static::assertSame('Woman', $result[3]->getTitle()); $this->translatableListener->setTranslatableLocale('lt_lt'); $result = $q->getResult(); - $this->assertCount(4, $result); - $this->assertEquals('Alfabetas', $result[0]->getTitle()); - $this->assertEquals('Kopustai', $result[1]->getTitle()); - $this->assertEquals('Maistas', $result[2]->getTitle()); - $this->assertEquals('Moteris', $result[3]->getTitle()); + static::assertCount(4, $result); + static::assertSame('Alfabetas', $result[0]->getTitle()); + static::assertSame('Kopustai', $result[1]->getTitle()); + static::assertSame('Maistas', $result[2]->getTitle()); + static::assertSame('Moteris', $result[3]->getTitle()); } - /** - * @test - */ - function shouldSelectOrderedByTranslatableInteger() + public function testShouldSelectOrderedByTranslatableInteger(): void { // Given $this->populateMore(); - $dql = 'SELECT a.title, a.views FROM '.self::ARTICLE.' a'; + $dql = 'SELECT a.title, a.views FROM '.Article::class.' a'; $dql .= ' ORDER BY a.views'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // Test original $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getArrayResult(); - array_walk($result, function ($value, $key) use (&$result) { + array_walk($result, static function ($value, $key) use (&$result) { // Make each record be a "Title - Views" string - $result[$key] = implode(" - ", $value); + $result[$key] = implode(' - ', $value); }); - $this->assertEquals( - array("Alfabet - 1", "Food - 99", "Cabbages - 2222", "Woman - 3333"), $result, - "Original of localizible integers should be sorted numerically" + static::assertSame( + ['Alfabet - 1', 'Food - 99', 'Cabbages - 2222', 'Woman - 3333'], $result, + 'Original of localizible integers should be sorted numerically' ); $this->translatableListener->setTranslatableLocale('lt_lt'); $result = $q->getArrayResult(); - array_walk($result, function ($value, $key) use (&$result) { + array_walk($result, static function ($value, $key) use (&$result) { // Make each record be a "Title - Views" string - $result[$key] = implode(" - ", $value); + $result[$key] = implode(' - ', $value); }); - $this->assertEquals( - array("Moteris - 33", "Alfabetas - 111", "Maistas - 999", "Kopustai - 22222"), $result, - "Localized integers should be sorted numerically" + static::assertSame( + ['Moteris - 33', 'Alfabetas - 111', 'Maistas - 999', 'Kopustai - 22222'], $result, + 'Localized integers should be sorted numerically' ); } - /** - * @test - */ - function shouldSelectSecondJoinedComponentTranslation() + public function testShouldSelectSecondJoinedComponentTranslation(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator')); - - $dql = 'SELECT a, c FROM '.self::ARTICLE.' a'; - $dql .= ' LEFT JOIN a.comments c'; + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_OBJECT_TRANSLATION, + ObjectHydrator::class + ); + + $dql = 'SELECT a, c FROM '.Article::class.' a'; + $dql .= ' LEFT JOIN a.comments c ORDER BY c.id ASC'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getArrayResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertCount(6, $food); - $this->assertEquals('Food', $food['title']); - $this->assertEquals('about food', $food['content']); + static::assertCount(6, $food); + static::assertSame('Food', $food['title']); + static::assertSame('about food', $food['content']); $comments = $food['comments']; - $this->assertCount(2, $comments); + static::assertCount(2, $comments); $good = $comments[0]; - $this->assertCount(3, $good); - $this->assertEquals('good', $good['subject']); - $this->assertEquals('food is good', $good['message']); + static::assertCount(3, $good); + static::assertSame('good', $good['subject']); + static::assertSame('food is good', $good['message']); $bad = $comments[1]; - $this->assertCount(3, $bad); - $this->assertEquals('bad', $bad['subject']); - $this->assertEquals('food is bad', $bad['message']); + static::assertCount(3, $bad); + static::assertSame('bad', $bad['subject']); + static::assertSame('food is bad', $bad['message']); $this->translatableListener->setTranslatableLocale('lt_lt'); $result = $q->getArrayResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertCount(6, $food); - $this->assertEquals('Maistas', $food['title']); - $this->assertEquals('apie maista', $food['content']); + static::assertCount(6, $food); + static::assertSame('Maistas', $food['title']); + static::assertSame('apie maista', $food['content']); $comments = $food['comments']; - $this->assertCount(2, $comments); + static::assertCount(2, $comments); $good = $comments[0]; - $this->assertCount(3, $good); - $this->assertEquals('geras', $good['subject']); - $this->assertEquals('maistas yra geras', $good['message']); + static::assertCount(3, $good); + static::assertSame('geras', $good['subject']); + static::assertSame('maistas yra geras', $good['message']); $bad = $comments[1]; - $this->assertCount(3, $bad); - $this->assertEquals('blogas', $bad['subject']); - $this->assertEquals('maistas yra blogas', $bad['message']); + static::assertCount(3, $bad); + static::assertSame('blogas', $bad['subject']); + static::assertSame('maistas yra blogas', $bad['message']); // object hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertEquals('Food', $food->getTitle()); - $this->assertEquals('about food', $food->getContent()); + static::assertSame('Food', $food->getTitle()); + static::assertSame('about food', $food->getContent()); $comments = $food->getComments(); - $this->assertCount(2, $comments); + static::assertCount(2, $comments); $good = $comments[0]; - $this->assertEquals('good', $good->getSubject()); - $this->assertEquals('food is good', $good->getMessage()); + static::assertSame('good', $good->getSubject()); + static::assertSame('food is good', $good->getMessage()); $bad = $comments[1]; - $this->assertEquals('bad', $bad->getSubject()); - $this->assertEquals('food is bad', $bad->getMessage()); + static::assertSame('bad', $bad->getSubject()); + static::assertSame('food is bad', $bad->getMessage()); $this->translatableListener->setTranslatableLocale('lt_lt'); $result = $q->getResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertEquals('Maistas', $food->getTitle()); - $this->assertEquals('apie maista', $food->getContent()); + static::assertSame('Maistas', $food->getTitle()); + static::assertSame('apie maista', $food->getContent()); $comments = $food->getComments(); - $this->assertCount(2, $comments); + static::assertCount(2, $comments); $good = $comments[0]; - $this->assertInstanceOf(self::COMMENT, $good); - $this->assertEquals('geras', $good->getSubject()); - $this->assertEquals('maistas yra geras', $good->getMessage()); + static::assertInstanceOf(Comment::class, $good); + static::assertSame('geras', $good->getSubject()); + static::assertSame('maistas yra geras', $good->getMessage()); $bad = $comments[1]; - $this->assertEquals('blogas', $bad->getSubject()); - $this->assertEquals('maistas yra blogas', $bad->getMessage()); + static::assertSame('blogas', $bad->getSubject()); + static::assertSame('maistas yra blogas', $bad->getMessage()); } - /** - * @test - */ - function shouldSelectSinglePartializedComponentTranslation() + public function testShouldSelectSinglePartializedComponentTranslation(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator')); - - $dql = 'SELECT a.title FROM '.self::ARTICLE.' a'; + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_OBJECT_TRANSLATION, + ObjectHydrator::class + ); + + $dql = 'SELECT a.title FROM '.Article::class.' a'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getArrayResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertCount(1, $food); - $this->assertEquals('Food', $food['title']); + static::assertCount(1, $food); + static::assertSame('Food', $food['title']); $this->translatableListener->setTranslatableLocale('lt_lt'); $result = $q->getArrayResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertCount(1, $food); - $this->assertEquals('Maistas', $food['title']); + static::assertCount(1, $food); + static::assertSame('Maistas', $food['title']); // object hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertCount(1, $food); - $this->assertEquals('Food', $food['title']); + static::assertCount(1, $food); + static::assertSame('Food', $food['title']); $this->translatableListener->setTranslatableLocale('lt_lt'); $result = $q->getResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertCount(1, $food); - $this->assertEquals('Maistas', $food['title']); + static::assertCount(1, $food); + static::assertSame('Maistas', $food['title']); } - /** - * @test - */ - function shouldSelectSingleComponentTranslation() + public function testShouldSelectSingleComponentTranslation(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator')); - - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_OBJECT_TRANSLATION, + ObjectHydrator::class + ); + + $dql = 'SELECT a FROM '.Article::class.' a'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getArrayResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertCount(5, $food); - $this->assertEquals('Food', $food['title']); - $this->assertEquals('about food', $food['content']); + static::assertCount(5, $food); + static::assertSame('Food', $food['title']); + static::assertSame('about food', $food['content']); $this->translatableListener->setTranslatableLocale('lt_lt'); $result = $q->getArrayResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertCount(5, $food); - $this->assertEquals('Maistas', $food['title']); - $this->assertEquals('apie maista', $food['content']); + static::assertCount(5, $food); + static::assertSame('Maistas', $food['title']); + static::assertSame('apie maista', $food['content']); // object hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertInstanceOf(self::ARTICLE, $food); - $this->assertEquals('Food', $food->getTitle()); - $this->assertEquals('about food', $food->getContent()); + static::assertInstanceOf(Article::class, $food); + static::assertSame('Food', $food->getTitle()); + static::assertSame('about food', $food->getContent()); $this->translatableListener->setTranslatableLocale('lt_lt'); $result = $q->getResult(); - $this->assertCount(1, $result); + static::assertCount(1, $result); $food = $result[0]; - $this->assertEquals('Maistas', $food->getTitle()); - $this->assertEquals('apie maista', $food->getContent()); + static::assertSame('Maistas', $food->getTitle()); + static::assertSame('apie maista', $food->getContent()); } /** - * @test * @group testSelectWithUnmappedField */ - function shouldSelectWithUnmappedField() + public function testShouldSelectWithUnmappedField(): void { - $dql = 'SELECT a.title, count(a.id) AS num FROM '.self::ARTICLE.' a'; + $dql = 'SELECT a.title, count(a.id) AS num FROM '.Article::class.' a'; $dql .= ' ORDER BY a.title'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); $result = $q->getArrayResult(); - $this->assertCount(1, $result); - $this->assertEquals('Food', $result[0]['title']); - $this->assertEquals(1, $result[0]['num']); + static::assertCount(1, $result); + static::assertSame('Food', $result[0]['title']); + static::assertSame(1, $result[0]['num']); } - /** - * @test - */ - function shouldPreserveSkipOnLoadForSimpleHydrator() + public function testShouldPreserveSkipOnLoadForSimpleHydrator(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_SIMPLE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\SimpleObjectHydrator')); - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_SIMPLE_OBJECT_TRANSLATION, + SimpleObjectHydrator::class + ); + $dql = 'SELECT a FROM '.Article::class.' a'; $dql .= ' ORDER BY a.title'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); $this->translatableListener->setSkipOnLoad(true); $q->getResult(Query::HYDRATE_SIMPLEOBJECT); - $this->assertTrue($this->translatableListener->isSkipOnLoad()); + static::assertTrue($this->translatableListener->isSkipOnLoad()); } - /** - * @test - */ - function shouldPreserveSkipOnLoadForObjectHydrator() + public function testShouldPreserveSkipOnLoadForObjectHydrator(): void { - $this->em - ->getConfiguration() - ->expects($this->any()) - ->method('getCustomHydrationMode') - ->with(TranslationWalker::HYDRATE_OBJECT_TRANSLATION) - ->will($this->returnValue('Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator')); - $dql = 'SELECT a FROM '.self::ARTICLE.' a'; + $this->em->getConfiguration()->addCustomHydrationMode( + TranslationWalker::HYDRATE_OBJECT_TRANSLATION, + ObjectHydrator::class + ); + $dql = 'SELECT a FROM '.Article::class.' a'; $dql .= ' ORDER BY a.title'; $q = $this->em->createQuery($dql); - $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::TREE_WALKER_TRANSLATION); + $q->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TranslationWalker::class); // array hydration $this->translatableListener->setTranslatableLocale('en_us'); $this->translatableListener->setSkipOnLoad(true); $q->getResult(Query::HYDRATE_OBJECT); - $this->assertTrue($this->translatableListener->isSkipOnLoad()); + static::assertTrue($this->translatableListener->isSkipOnLoad()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - self::TRANSLATION, - self::COMMENT, - ); + return [ + Article::class, + Translation::class, + Comment::class, + ]; } - private function populateMore() + private function populateMore(): void { - $repo = $this->em->getRepository(self::ARTICLE); - $commentRepo = $this->em->getRepository(self::COMMENT); + $repo = $this->em->getRepository(Article::class); $this->translatableListener->setTranslatableLocale('en_us'); $alfabet = new Article(); @@ -783,10 +742,10 @@ private function populateMore() $this->em->clear(); } - private function populate() + private function populate(): void { - $repo = $this->em->getRepository(self::ARTICLE); - $commentRepo = $this->em->getRepository(self::COMMENT); + $repo = $this->em->getRepository(Article::class); + $commentRepo = $this->em->getRepository(Comment::class); $food = new Article(); $food->setTitle('Food'); diff --git a/tests/Gedmo/Translator/Fixture/CustomProxy.php b/tests/Gedmo/Translator/Fixture/CustomProxy.php index 51bc7e26e9..dff953290b 100644 --- a/tests/Gedmo/Translator/Fixture/CustomProxy.php +++ b/tests/Gedmo/Translator/Fixture/CustomProxy.php @@ -1,17 +1,26 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translator\Fixture; use Gedmo\Translator\TranslationProxy; class CustomProxy extends TranslationProxy { - public function setName($name) + public function setName(?string $name): void { - return $this->setTranslatedValue('name', $name); + $this->setTranslatedValue('name', $name); } - public function getName() + public function getName(): ?string { return $this->getTranslatedValue('name'); } diff --git a/tests/Gedmo/Translator/Fixture/Person.php b/tests/Gedmo/Translator/Fixture/Person.php index bdbce7fd52..7db47153da 100644 --- a/tests/Gedmo/Translator/Fixture/Person.php +++ b/tests/Gedmo/Translator/Fixture/Person.php @@ -1,113 +1,144 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translator\Fixture; -use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Translator\TranslationInterface; +use Gedmo\Translator\TranslationProxy; /** * @ORM\Entity */ +#[ORM\Entity] class Person { /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - private $id; - - /** + * @var string|null + * * @ORM\Column(name="name", type="string", length=128) */ + #[ORM\Column(name: 'name', type: Types::STRING, length: 128)] public $name; /** + * @var string|null + * * @ORM\Column(name="desc", type="string", length=128) */ + #[ORM\Column(name: 'desc', type: Types::STRING, length: 128)] public $description; /** + * @var string|null + * * @ORM\Column(name="last_name", type="string", length=128, nullable=true) */ + #[ORM\Column(name: 'last_name', type: Types::STRING, length: 128, nullable: true)] public $lastName; - public function getId() + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="PersonTranslation", mappedBy="translatable", cascade={"persist"}) + */ + #[ORM\OneToMany(targetEntity: PersonTranslation::class, mappedBy: 'translatable', cascade: ['persist'])] + private Collection $translations; + + /** + * @ORM\ManyToOne(targetEntity="Person") + */ + #[ORM\ManyToOne(targetEntity: self::class)] + private ?Person $parent = null; + + public function __construct() + { + $this->translations = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setDescription($description) + public function setDescription(?string $description): void { $this->description = $description; } - public function getDescription() + public function getDescription(): ?string { return $this->description; } - public function getLastName() + public function getLastName(): ?string { return $this->lastName; } - public function setLastName($name) + public function setLastName(?string $name): void { $this->lastName = $name; } - // - // TRANSLATIONS DEFINITION: - // - - - /** - * @ORM\OneToMany(targetEntity="PersonTranslation", mappedBy="translatable", cascade={"persist"}) - */ - private $translations; - - /** - * @ORM\ManyToOne(targetEntity="Person") - */ - private $parent; - - public function setParent(Person $parent) + public function setParent(self $parent): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function __construct() - { - $this->translations = new ArrayCollection(); - } - - public function translate($locale = 'en') + /** + * @return self|TranslationProxy + */ + public function translate(string $locale = 'en') { if ('en' === $locale) { return $this; } - return new \Gedmo\Translator\TranslationProxy($this, - /* Locale */ $locale, - /* List of translatable properties: */ array('name', 'lastName'), - /* Translation entity class: */ 'Translator\Fixture\PersonTranslation', - /* Translations collection property: */ $this->translations + return new TranslationProxy( + $this, + $locale, // Locale + ['name', 'lastName'], // List of translatable properties + PersonTranslation::class, // Translation entity class + $this->translations // Translations collection property ); } } diff --git a/tests/Gedmo/Translator/Fixture/PersonCustom.php b/tests/Gedmo/Translator/Fixture/PersonCustom.php index 536641a1a3..dd7395fdfa 100644 --- a/tests/Gedmo/Translator/Fixture/PersonCustom.php +++ b/tests/Gedmo/Translator/Fixture/PersonCustom.php @@ -1,83 +1,105 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translator\Fixture; -use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Translator\TranslationInterface; /** * @ORM\Entity */ +#[ORM\Entity] class PersonCustom { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="name", type="string", length=128) */ - private $name; + #[ORM\Column(name: 'name', type: Types::STRING, length: 128)] + private ?string $name = null; /** * @ORM\Column(name="desc", type="string", length=128) */ - private $description; + #[ORM\Column(name: 'desc', type: Types::STRING, length: 128)] + private ?string $description = null; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="PersonCustomTranslation", mappedBy="translatable", cascade={"persist"}) + */ + #[ORM\OneToMany(targetEntity: PersonCustomTranslation::class, mappedBy: 'translatable', cascade: ['persist'])] + private Collection $translations; - public function getId() + public function __construct() + { + $this->translations = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setDescription($description) + public function setDescription(?string $description): void { $this->description = $description; } - public function getDescription() + public function getDescription(): ?string { return $this->description; } - // - // TRANSLATIONS DEFINITION: - // - - /** - * @ORM\OneToMany(targetEntity="PersonCustomTranslation", mappedBy="translatable", cascade={"persist"}) + * @return self|CustomProxy */ - private $translations; - - public function __construct() - { - $this->translations = new ArrayCollection(); - } - - public function translate($locale = null) + public function translate(?string $locale = null) { if (null === $locale) { return $this; } - return new CustomProxy($this, - /* Locale */ $locale, - /* List of translatable properties: */ array('name'), - /* Translation entity class: */ 'Translator\Fixture\PersonCustomTranslation', - /* Translations collection property: */ $this->translations + return new CustomProxy( + $this, + $locale, // Locale + ['name'], // List of translatable properties + PersonCustomTranslation::class, // Translation entity class + $this->translations // Translations collection property ); } } diff --git a/tests/Gedmo/Translator/Fixture/PersonCustomTranslation.php b/tests/Gedmo/Translator/Fixture/PersonCustomTranslation.php index 442b25596c..5a12baf6d5 100644 --- a/tests/Gedmo/Translator/Fixture/PersonCustomTranslation.php +++ b/tests/Gedmo/Translator/Fixture/PersonCustomTranslation.php @@ -1,25 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translator\Fixture; use Doctrine\ORM\Mapping as ORM; use Gedmo\Translator\Entity\Translation; /** * @ORM\Table( - * indexes={@ORM\Index(name="pers_translations_lookup_idx", columns={ - * "locale", "translatable_id" - * })}, - * uniqueConstraints={@ORM\UniqueConstraint(name="pers_lookup_unique_idx", columns={ - * "locale", "translatable_id", "property" - * })} + * indexes={@ORM\Index(name="pers_translations_lookup_idx", columns={ + * "locale", "translatable_id" + * })}, + * uniqueConstraints={@ORM\UniqueConstraint(name="pers_lookup_unique_idx", columns={ + * "locale", "translatable_id", "property" + * })} * ) * @ORM\Entity */ +#[ORM\Entity] +#[ORM\Index(name: 'pers_translations_lookup_idx', columns: ['locale', 'translatable_id'])] +#[ORM\UniqueConstraint(name: 'pers_lookup_unique_idx', columns: ['locale', 'translatable_id', 'property'])] class PersonCustomTranslation extends Translation { /** + * @var PersonCustom|null + * * @ORM\ManyToOne(targetEntity="PersonCustom", inversedBy="translations") */ + #[ORM\ManyToOne(targetEntity: PersonCustom::class, inversedBy: 'translations')] protected $translatable; } diff --git a/tests/Gedmo/Translator/Fixture/PersonTranslation.php b/tests/Gedmo/Translator/Fixture/PersonTranslation.php index 269884f8e4..8a920b0b3f 100644 --- a/tests/Gedmo/Translator/Fixture/PersonTranslation.php +++ b/tests/Gedmo/Translator/Fixture/PersonTranslation.php @@ -1,25 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translator\Fixture; use Doctrine\ORM\Mapping as ORM; use Gedmo\Translator\Entity\Translation; /** * @ORM\Table( - * indexes={@ORM\Index(name="translations_lookup_idx", columns={ - * "locale", "translatable_id" - * })}, - * uniqueConstraints={@ORM\UniqueConstraint(name="lookup_unique_idx", columns={ - * "locale", "translatable_id", "property" - * })} + * indexes={@ORM\Index(name="translations_lookup_idx", columns={ + * "locale", "translatable_id" + * })}, + * uniqueConstraints={@ORM\UniqueConstraint(name="lookup_unique_idx", columns={ + * "locale", "translatable_id", "property" + * })} * ) * @ORM\Entity */ +#[ORM\Entity] +#[ORM\Index(name: 'translations_lookup_idx', columns: ['locale', 'translatable_id'])] +#[ORM\UniqueConstraint(name: 'lookup_unique_idx', columns: ['locale', 'translatable_id', 'property'])] class PersonTranslation extends Translation { /** + * @var Person|null + * * @ORM\ManyToOne(targetEntity="Person", inversedBy="translations") */ + #[ORM\ManyToOne(targetEntity: Person::class, inversedBy: 'translations')] protected $translatable; } diff --git a/tests/Gedmo/Translator/TranslatableTest.php b/tests/Gedmo/Translator/TranslatableTest.php index 4eba9f4fda..b89ea2ef7a 100644 --- a/tests/Gedmo/Translator/TranslatableTest.php +++ b/tests/Gedmo/Translator/TranslatableTest.php @@ -1,34 +1,37 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Translator; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Translator\Fixture\Person; -use Translator\Fixture\PersonCustom; -use Doctrine\ORM\Proxy\Proxy; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Translator\Fixture\Person; +use Gedmo\Tests\Translator\Fixture\PersonCustom; /** * These are tests for translatable behavior * * @author Konstantin Kudryashov - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableTest extends BaseTestCaseORM +final class TranslatableTest extends BaseTestCaseORM { - const PERSON = 'Translator\\Fixture\\Person'; - const PERSON_CUSTOM_PROXY = 'Translator\\Fixture\\PersonCustom'; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testTranslatable() + public function testTranslatable(): void { $person = new Person(); $person->setName('Jen'); @@ -36,60 +39,57 @@ public function testTranslatable() $person->setDescription('description'); $person->translate('ru_RU')->setDescription('multilingual description'); - $this->assertSame('Jen', $person->getName()); - $this->assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); - $this->assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); - $this->assertSame('multilingual description', $person->getDescription()); + static::assertSame('Jen', $person->getName()); + static::assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); + static::assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); + static::assertSame('multilingual description', $person->getDescription()); $this->em->persist($person); $this->em->flush(); $this->em->clear(); // retrieve record (translations would be fetched later - by demand) - $person = $this->em->getRepository(self::PERSON)->findOneByName('Jen'); + $person = $this->em->getRepository(Person::class)->findOneBy(['name' => 'Jen']); - $this->assertSame('Jen', $person->getName()); - $this->assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); - $this->assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); - $this->assertSame('multilingual description', $person->getDescription()); + static::assertSame('Jen', $person->getName()); + static::assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); + static::assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); + static::assertSame('multilingual description', $person->getDescription()); // retrieve record with all translations in one query - $persons = $this->em->getRepository(self::PERSON) + $persons = $this->em->getRepository(Person::class) ->createQueryBuilder('p') ->select('p, t') ->join('p.translations', 't') ->getQuery() - ->execute(); + ->getResult(); $person = $persons[0]; - $this->assertSame('Jen', $person->getName()); - $this->assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); - $this->assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); - $this->assertSame('multilingual description', $person->getDescription()); + static::assertSame('Jen', $person->getName()); + static::assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); + static::assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); + static::assertSame('multilingual description', $person->getDescription()); $person->translate('es_ES')->setName('Amigo'); $this->em->flush(); // retrieve record with all translations in one query - $persons = $this->em->getRepository(self::PERSON) + $persons = $this->em->getRepository(Person::class) ->createQueryBuilder('p') ->select('p, t') ->join('p.translations', 't') ->getQuery() - ->execute(); + ->getResult(); $person = $persons[0]; - $this->assertSame('Jen', $person->getName()); - $this->assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); - $this->assertSame('Amigo', $person->translate('es_ES')->getName()); - $this->assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); + static::assertSame('Jen', $person->getName()); + static::assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); + static::assertSame('Amigo', $person->translate('es_ES')->getName()); + static::assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); } - /** - * @test - */ - public function shouldTranslateRelation() + public function testShouldTranslateRelation(): void { $person = new Person(); $person->setName('Jen'); @@ -98,7 +98,7 @@ public function shouldTranslateRelation() $person->translate('ru')->setDescription('multilingual description'); $parent = new Person(); - $parent->setName('Jen'); + $parent->setName('Jen parent'); $parent->translate('ru')->setName('ะ–ะตะฝั starshai'); $parent->translate('fr')->setName('zenia'); $parent->setDescription('description'); @@ -110,18 +110,16 @@ public function shouldTranslateRelation() $this->em->flush(); $this->em->clear(); - $person = $this->em->getRepository(self::PERSON)->findOneByName('Jen'); - $this->assertSame('ะ–ะตะฝั', $person->translate('ru')->getName()); + $person = $this->em->getRepository(Person::class)->findOneBy(['name' => 'Jen']); + static::assertSame('ะ–ะตะฝั', $person->translate('ru')->getName()); $parent = $person->getParent(); - $this->assertTrue($parent instanceof Proxy); - $this->assertSame('ะ–ะตะฝั starshai', $parent->translate('ru')->getName()); - $this->assertSame('zenia', $parent->translate('fr')->getName()); + static::assertTrue($this->em->isUninitializedObject($parent)); + static::assertInstanceOf(Person::class, $parent); + static::assertSame('ะ–ะตะฝั starshai', $parent->translate('ru')->getName()); + static::assertSame('zenia', $parent->translate('fr')->getName()); } - /** - * @test - */ - public function shouldHandleDomainObjectProxy() + public function testShouldHandleDomainObjectProxy(): void { $person = new Person(); $person->setName('Jen'); @@ -133,13 +131,13 @@ public function shouldHandleDomainObjectProxy() $this->em->flush(); $this->em->clear(); - $personProxy = $this->em->getReference(self::PERSON, array('id' => 1)); - $this->assertTrue($personProxy instanceof Proxy); + $personProxy = $this->em->getReference(Person::class, ['id' => 1]); + static::assertTrue($this->em->isUninitializedObject($personProxy)); $name = $personProxy->translate('ru_RU')->getName(); - $this->assertSame('ะ–ะตะฝั', $name); + static::assertSame('ะ–ะตะฝั', $name); } - public function testTranslatableProxyWithUpperCaseProperty() + public function testTranslatableProxyWithUpperCaseProperty(): void { $person = new Person(); $person->setName('Jen'); @@ -153,29 +151,29 @@ public function testTranslatableProxyWithUpperCaseProperty() $this->em->flush(); $this->em->clear(); - $personProxy = $this->em->getReference(self::PERSON, array('id' => 1)); - $this->assertTrue($personProxy instanceof Proxy); + $personProxy = $this->em->getReference(Person::class, ['id' => 1]); + static::assertTrue($this->em->isUninitializedObject($personProxy)); $name = $personProxy->translate('ru_RU')->getName(); - $this->assertSame('ะ–ะตะฝั', $name); + static::assertSame('ะ–ะตะฝั', $name); $lastName = $personProxy->translate('ru_RU')->getLastName(); - $this->assertSame('ะะฑั€ะฐะผะพะฒะธั‡', $lastName); + static::assertSame('ะะฑั€ะฐะผะพะฒะธั‡', $lastName); } - public function testTranslatableWithMagicProperties() + public function testTranslatableWithMagicProperties(): void { $person = new Person(); $person->translate('en')->setName('Jen'); $person->translate('ru_RU')->name = 'ะ–ะตะฝั'; $person->translate('ru_RU')->description = 'multilingual description'; - $this->assertSame('Jen', $person->name); - $this->assertSame('Jen', $person->translate()->name); - $this->assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->name); - $this->assertSame('multilingual description', $person->translate('ru_RU')->description); - $this->assertSame('multilingual description', $person->description); + static::assertSame('Jen', $person->name); + static::assertSame('Jen', $person->translate()->name); + static::assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->name); + static::assertSame('multilingual description', $person->translate('ru_RU')->description); + static::assertSame('multilingual description', $person->description); } - public function testTranslatableWithCustomProxy() + public function testTranslatableWithCustomProxy(): void { $person = new PersonCustom(); $person->setName('Jen'); @@ -183,61 +181,61 @@ public function testTranslatableWithCustomProxy() $person->setDescription('description'); $person->translate('ru_RU')->setDescription('multilingual description'); - $this->assertSame('Jen', $person->getName()); - $this->assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); - $this->assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); - $this->assertSame('multilingual description', $person->getDescription()); + static::assertSame('Jen', $person->getName()); + static::assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); + static::assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); + static::assertSame('multilingual description', $person->getDescription()); $this->em->persist($person); $this->em->flush(); $this->em->clear(); // retrieve record (translations would be fetched later - by demand) - $person = $this->em->getRepository(self::PERSON_CUSTOM_PROXY)->findOneByName('Jen'); + $person = $this->em->getRepository(PersonCustom::class)->findOneBy(['name' => 'Jen']); - $this->assertSame('Jen', $person->getName()); - $this->assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); - $this->assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); - $this->assertSame('multilingual description', $person->getDescription()); + static::assertSame('Jen', $person->getName()); + static::assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); + static::assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); + static::assertSame('multilingual description', $person->getDescription()); // retrieve record with all translations in one query - $persons = $this->em->getRepository(self::PERSON_CUSTOM_PROXY) + $persons = $this->em->getRepository(PersonCustom::class) ->createQueryBuilder('p') ->select('p, t') ->join('p.translations', 't') ->getQuery() - ->execute(); + ->getResult(); $person = $persons[0]; - $this->assertSame('Jen', $person->getName()); - $this->assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); - $this->assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); - $this->assertSame('multilingual description', $person->getDescription()); + static::assertSame('Jen', $person->getName()); + static::assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); + static::assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); + static::assertSame('multilingual description', $person->getDescription()); $person->translate('es_ES')->setName('Amigo'); $this->em->flush(); // retrieve record with all translations in one query - $persons = $this->em->getRepository(self::PERSON_CUSTOM_PROXY) + $persons = $this->em->getRepository(PersonCustom::class) ->createQueryBuilder('p') ->select('p, t') ->join('p.translations', 't') ->getQuery() - ->execute(); + ->getResult(); $person = $persons[0]; - $this->assertSame('Jen', $person->getName()); - $this->assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); - $this->assertSame('Amigo', $person->translate('es_ES')->getName()); - $this->assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); + static::assertSame('Jen', $person->getName()); + static::assertSame('ะ–ะตะฝั', $person->translate('ru_RU')->getName()); + static::assertSame('Amigo', $person->translate('es_ES')->getName()); + static::assertSame('multilingual description', $person->translate('ru_RU')->getDescription()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::PERSON, self::PERSON.'Translation', - self::PERSON_CUSTOM_PROXY, self::PERSON_CUSTOM_PROXY.'Translation', - ); + return [ + Person::class, Person::class.'Translation', + PersonCustom::class, PersonCustom::class.'Translation', + ]; } } diff --git a/tests/Gedmo/Tree/ClosureTreeRepositoryTest.php b/tests/Gedmo/Tree/ClosureTreeRepositoryTest.php index 4d871d596c..d05ab778b6 100644 --- a/tests/Gedmo/Tree/ClosureTreeRepositoryTest.php +++ b/tests/Gedmo/Tree/ClosureTreeRepositoryTest.php @@ -1,31 +1,43 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\Closure\Category; -use Tree\Fixture\Closure\CategoryWithoutLevel; -use Tree\Fixture\Closure\CategoryWithoutLevelClosure; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Closure\Category; +use Gedmo\Tests\Tree\Fixture\Closure\CategoryClosure; +use Gedmo\Tests\Tree\Fixture\Closure\CategoryWithoutLevel; +use Gedmo\Tests\Tree\Fixture\Closure\CategoryWithoutLevelClosure; +use Gedmo\Tree\Entity\Repository\AbstractTreeRepository; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gustavo Adrian * @author Gediminas Morkevicius - * @link http://www.gediminasm.org + * + * @see http://www.gediminasm.org + * * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ClosureTreeRepositoryTest extends BaseTestCaseORM +final class ClosureTreeRepositoryTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\Closure\\Category"; - const CLOSURE = "Tree\\Fixture\\Closure\\CategoryClosure"; - const CATEGORY_WITHOUT_LEVEL = "Tree\\Fixture\\Closure\\CategoryWithoutLevel"; - const CATEGORY_WITHOUT_LEVEL_CLOSURE = "Tree\\Fixture\\Closure\\CategoryWithoutLevelClosure"; - + /** + * @var TreeListener + */ protected $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -34,199 +46,243 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($this->listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testChildCount() + public function testChildCount(): void { $this->populate(); - $repo = $this->em->getRepository(self::CATEGORY); - $food = $repo->findOneByTitle('Food'); + $repo = $this->em->getRepository(Category::class); + $food = $repo->findOneBy(['title' => 'Food']); // Count all $count = $repo->childCount(); - $this->assertEquals(15, $count); + static::assertSame(15, $count); // Count all, but only direct ones $count = $repo->childCount(null, true); - $this->assertEquals(2, $count); + static::assertSame(2, $count); // Count food children - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $count = $repo->childCount($food); - $this->assertEquals(11, $count); + static::assertSame(11, $count); // Count food children, but only direct ones - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $count = $repo->childCount($food, true); - $this->assertEquals(3, $count); + static::assertSame(3, $count); } - public function testPath() + public function testPath(): void { $this->populate(); - $repo = $this->em->getRepository(self::CATEGORY); - $fruits = $repo->findOneByTitle('Fruits'); + $repo = $this->em->getRepository(Category::class); + $fruits = $repo->findOneBy(['title' => 'Fruits']); $path = $repo->getPath($fruits); - $this->assertCount(2, $path); - $this->assertEquals('Food', $path[0]->getTitle()); - $this->assertEquals('Fruits', $path[1]->getTitle()); + static::assertCount(2, $path); + static::assertSame('Food', $path[0]->getTitle()); + static::assertSame('Fruits', $path[1]->getTitle()); - $strawberries = $repo->findOneByTitle('Strawberries'); + $strawberries = $repo->findOneBy(['title' => 'Strawberries']); $path = $repo->getPath($strawberries); - $this->assertCount(4, $path); - $this->assertEquals('Food', $path[0]->getTitle()); - $this->assertEquals('Fruits', $path[1]->getTitle()); - $this->assertEquals('Berries', $path[2]->getTitle()); - $this->assertEquals('Strawberries', $path[3]->getTitle()); + static::assertCount(4, $path); + static::assertSame('Food', $path[0]->getTitle()); + static::assertSame('Fruits', $path[1]->getTitle()); + static::assertSame('Berries', $path[2]->getTitle()); + static::assertSame('Strawberries', $path[3]->getTitle()); } - public function testChildren() + public function testChildren(): void { $this->populate(); - $repo = $this->em->getRepository(self::CATEGORY); - $fruits = $repo->findOneByTitle('Fruits'); + $repo = $this->em->getRepository(Category::class); + $fruits = $repo->findOneBy(['title' => 'Fruits']); // direct children of node, sorted by title ascending order. NOT including the root node $children = $repo->children($fruits, true, 'title'); - $this->assertCount(3, $children); - $this->assertEquals('Berries', $children[0]->getTitle()); - $this->assertEquals('Lemons', $children[1]->getTitle()); - $this->assertEquals('Oranges', $children[2]->getTitle()); + static::assertCount(3, $children); + static::assertSame('Berries', $children[0]->getTitle()); + static::assertSame('Lemons', $children[1]->getTitle()); + static::assertSame('Oranges', $children[2]->getTitle()); // direct children of node, sorted by title ascending order. including the root node $children = $repo->children($fruits, true, 'title', 'asc', true); - $this->assertCount(4, $children); - $this->assertEquals('Berries', $children[0]->getTitle()); - $this->assertEquals('Fruits', $children[1]->getTitle()); - $this->assertEquals('Lemons', $children[2]->getTitle()); - $this->assertEquals('Oranges', $children[3]->getTitle()); + static::assertCount(4, $children); + static::assertSame('Berries', $children[0]->getTitle()); + static::assertSame('Fruits', $children[1]->getTitle()); + static::assertSame('Lemons', $children[2]->getTitle()); + static::assertSame('Oranges', $children[3]->getTitle()); // all children of node, NOT including the root $children = $repo->children($fruits); - $this->assertCount(4, $children); - $this->assertEquals('Oranges', $children[0]->getTitle()); - $this->assertEquals('Lemons', $children[1]->getTitle()); - $this->assertEquals('Berries', $children[2]->getTitle()); - $this->assertEquals('Strawberries', $children[3]->getTitle()); + static::assertCount(4, $children); + static::assertSame('Oranges', $children[0]->getTitle()); + static::assertSame('Lemons', $children[1]->getTitle()); + static::assertSame('Berries', $children[2]->getTitle()); + static::assertSame('Strawberries', $children[3]->getTitle()); // all children of node, including the root $children = $repo->children($fruits, false, 'title', 'asc', true); - $this->assertCount(5, $children); - $this->assertEquals('Berries', $children[0]->getTitle()); - $this->assertEquals('Fruits', $children[1]->getTitle()); - $this->assertEquals('Lemons', $children[2]->getTitle()); - $this->assertEquals('Oranges', $children[3]->getTitle()); - $this->assertEquals('Strawberries', $children[4]->getTitle()); + static::assertCount(5, $children); + static::assertSame('Berries', $children[0]->getTitle()); + static::assertSame('Fruits', $children[1]->getTitle()); + static::assertSame('Lemons', $children[2]->getTitle()); + static::assertSame('Oranges', $children[3]->getTitle()); + static::assertSame('Strawberries', $children[4]->getTitle()); + + // test children sorting by array of fields + $children = $repo->children($fruits, false, ['title'], 'ASC', true); + static::assertCount(5, $children); + static::assertSame('Berries', $children[0]->getTitle()); + static::assertSame('Fruits', $children[1]->getTitle()); + static::assertSame('Lemons', $children[2]->getTitle()); + static::assertSame('Oranges', $children[3]->getTitle()); + static::assertSame('Strawberries', $children[4]->getTitle()); + + $children = $repo->children($fruits, false, ['level', 'title'], ['ASC', 'DESC'], true); + static::assertCount(5, $children); + static::assertSame('Fruits', $children[0]->getTitle()); + static::assertSame('Oranges', $children[1]->getTitle()); + static::assertSame('Lemons', $children[2]->getTitle()); + static::assertSame('Berries', $children[3]->getTitle()); + static::assertSame('Strawberries', $children[4]->getTitle()); + + $children = $repo->children($fruits, false, ['level', 'title'], ['ASC'], true); + static::assertCount(5, $children); + static::assertSame('Fruits', $children[0]->getTitle()); + static::assertSame('Berries', $children[1]->getTitle()); + static::assertSame('Lemons', $children[2]->getTitle()); + static::assertSame('Oranges', $children[3]->getTitle()); + static::assertSame('Strawberries', $children[4]->getTitle()); + + // test sorting by single-valued association field + $children = $repo->children($fruits, false, 'parent'); + static::assertCount(4, $children); + static::assertSame('Oranges', $children[0]->getTitle()); + static::assertSame('Lemons', $children[1]->getTitle()); + static::assertSame('Berries', $children[2]->getTitle()); + static::assertSame('Strawberries', $children[3]->getTitle()); + + $children = $repo->children($fruits, false, ['parent'], ['ASC']); + static::assertCount(4, $children); + static::assertSame('Oranges', $children[0]->getTitle()); + static::assertSame('Lemons', $children[1]->getTitle()); + static::assertSame('Berries', $children[2]->getTitle()); + static::assertSame('Strawberries', $children[3]->getTitle()); // direct root nodes $children = $repo->children(null, true, 'title'); - $this->assertCount(2, $children); - $this->assertEquals('Food', $children[0]->getTitle()); - $this->assertEquals('Sports', $children[1]->getTitle()); + static::assertCount(2, $children); + static::assertSame('Food', $children[0]->getTitle()); + static::assertSame('Sports', $children[1]->getTitle()); // all tree $children = $repo->children(); - $this->assertCount(15, $children); + static::assertCount(15, $children); } - public function testSingleNodeRemoval() + public function testSingleNodeRemoval(): void { $this->populate(); - $repo = $this->em->getRepository(self::CATEGORY); - $fruits = $repo->findOneByTitle('Fruits'); + $repo = $this->em->getRepository(Category::class); + $fruits = $repo->findOneBy(['title' => 'Fruits']); $repo->removeFromTree($fruits); // ensure in memory node integrity $this->em->flush(); - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $children = $repo->children($food, true); - $this->assertCount(5, $children); + static::assertCount(5, $children); - $berries = $repo->findOneByTitle('Berries'); - $this->assertEquals(1, $repo->childCount($berries, true)); + $berries = $repo->findOneBy(['title' => 'Berries']); + static::assertSame(1, $repo->childCount($berries, true)); - $lemons = $repo->findOneByTitle('Lemons'); - $this->assertEquals(0, $repo->childCount($lemons, true)); + $lemons = $repo->findOneBy(['title' => 'Lemons']); + static::assertSame(0, $repo->childCount($lemons, true)); $repo->removeFromTree($food); - $vegitables = $repo->findOneByTitle('Vegitables'); - $this->assertEquals(2, $repo->childCount($vegitables, true)); - $this->assertNull($vegitables->getParent()); + $vegitables = $repo->findOneBy(['title' => 'Vegitables']); + static::assertSame(2, $repo->childCount($vegitables, true)); + static::assertNull($vegitables->getParent()); $repo->removeFromTree($lemons); - $this->assertCount(5, $repo->children(null, true)); + static::assertCount(5, $repo->children(null, true)); } - public function testBuildTreeWithLevelProperty() + public function testBuildTreeWithLevelProperty(): void { $this->populate(); - $this->buildTreeTests(self::CATEGORY); + $this->buildTreeTests(Category::class); } - public function testBuildTreeWithoutLevelProperty() + public function testBuildTreeWithoutLevelProperty(): void { - $this->populate(self::CATEGORY_WITHOUT_LEVEL); + $this->populate(CategoryWithoutLevel::class); - $this->buildTreeTests(self::CATEGORY_WITHOUT_LEVEL); + $this->buildTreeTests(CategoryWithoutLevel::class); } - public function testHavingLevelPropertyAvoidsSubqueryInSelectInGetNodesHierarchy() + public function testHavingLevelPropertyAvoidsSubqueryInSelectInGetNodesHierarchy(): void { $this->populate(); - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(Category::class); $roots = $repo->getRootNodes(); - $meta = $this->em->getClassMetadata(self::CATEGORY); - $config = $this->listener->getConfiguration($this->em, $meta->name); + $meta = $this->em->getClassMetadata(Category::class); + $config = $this->listener->getConfiguration($this->em, $meta->getName()); $qb = $repo->getNodesHierarchyQueryBuilder($roots[0], false, $config); - $this->assertFalse(strpos($qb->getQuery()->getDql(), '(SELECT MAX(')); + static::assertFalse(strpos($qb->getQuery()->getDql(), '(SELECT MAX(')); } - public function testNotHavingLevelPropertyUsesASubqueryInSelectInGetNodesHierarchy() + public function testNotHavingLevelPropertyUsesASubqueryInSelectInGetNodesHierarchy(): void { - $this->populate(self::CATEGORY_WITHOUT_LEVEL); + $this->populate(CategoryWithoutLevel::class); - $repo = $this->em->getRepository(self::CATEGORY_WITHOUT_LEVEL); + $repo = $this->em->getRepository(CategoryWithoutLevel::class); $roots = $repo->getRootNodes(); - $meta = $this->em->getClassMetadata(self::CATEGORY_WITHOUT_LEVEL); - $config = $this->listener->getConfiguration($this->em, $meta->name); + $meta = $this->em->getClassMetadata(CategoryWithoutLevel::class); + $config = $this->listener->getConfiguration($this->em, $meta->getName()); $qb = $repo->getNodesHierarchyQueryBuilder($roots[0], false, $config); - $this->assertTrue(((bool) strpos($qb->getQuery()->getDql(), '(SELECT MAX('))); + static::assertTrue((bool) strpos($qb->getQuery()->getDql(), '(SELECT MAX(')); } - public function test_changeChildrenIndex() + public function testChangeChildrenIndex(): void { - $this->populate(self::CATEGORY); + $this->populate(Category::class); $childrenIndex = 'myChildren'; - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(Category::class); $repo->setChildrenIndex($childrenIndex); $tree = $repo->childrenHierarchy(); - $this->assertInternalType('array', $tree[0][$childrenIndex]); + static::assertIsArray($tree[0][$childrenIndex]); } // Utility Methods - protected function buildTreeTests($class) + /** + * @phpstan-param class-string $class + */ + protected function buildTreeTests(string $class): void { $repo = $this->em->getRepository($class); - $sortOption = array('childSort' => array('field' => 'title', 'dir' => 'asc')); + static::assertInstanceOf(AbstractTreeRepository::class, $repo); + $sortOption = ['childSort' => ['field' => 'title', 'dir' => 'asc']]; - $testClosure = function (ClosureTreeRepositoryTest $phpUnit, array $tree, $includeNode = false, $whichTree = 'both', $includeNewNode = false) { - if ($whichTree === 'both' || $whichTree === 'first') { + $testClosure = static function (array $tree, $includeNode = false, $whichTree = 'both', $includeNewNode = false): void { + if ('both' === $whichTree || 'first' === $whichTree) { $boringFood = $includeNewNode ? ($includeNode ? $tree[0]['__children'][0] : $tree[0]) : null; $fruitsIndex = $includeNewNode ? 1 : 0; $milkIndex = $includeNewNode ? 2 : 1; @@ -235,42 +291,42 @@ protected function buildTreeTests($class) $vegitables = $includeNewNode ? $boringFood['__children'][0] : ($includeNode ? $tree[0]['__children'][2] : $tree[2]); if ($includeNode) { - $phpUnit->assertEquals('Food', $tree[0]['title']); + static::assertSame('Food', $tree[0]['title']); } - $phpUnit->assertEquals('Fruits', $fruits['title']); - $phpUnit->assertEquals('Berries', $fruits['__children'][0]['title']); - $phpUnit->assertEquals('Strawberries', $fruits['__children'][0]['__children'][0]['title']); - $phpUnit->assertEquals('Milk', $milk['title']); - $phpUnit->assertEquals('Cheese', $milk['__children'][0]['title']); - $phpUnit->assertEquals('Mould cheese', $milk['__children'][0]['__children'][0]['title']); + static::assertSame('Fruits', $fruits['title']); + static::assertSame('Berries', $fruits['__children'][0]['title']); + static::assertSame('Strawberries', $fruits['__children'][0]['__children'][0]['title']); + static::assertSame('Milk', $milk['title']); + static::assertSame('Cheese', $milk['__children'][0]['title']); + static::assertSame('Mould cheese', $milk['__children'][0]['__children'][0]['title']); if ($boringFood) { - $phpUnit->assertEquals('Boring Food', $boringFood['title']); + static::assertSame('Boring Food', $boringFood['title']); } - $phpUnit->assertEquals('Vegitables', $vegitables['title']); - $phpUnit->assertEquals('Cabbages', $vegitables['__children'][0]['title']); - $phpUnit->assertEquals('Carrots', $vegitables['__children'][1]['title']); + static::assertSame('Vegitables', $vegitables['title']); + static::assertSame('Cabbages', $vegitables['__children'][0]['title']); + static::assertSame('Carrots', $vegitables['__children'][1]['title']); } - if ($whichTree === 'both' || $whichTree === 'second') { - $root = $whichTree === 'both' ? $tree[1] : $tree[0]; + if ('both' === $whichTree || 'second' === $whichTree) { + $root = 'both' === $whichTree ? $tree[1] : $tree[0]; $soccer = $includeNode ? $root['__children'][0] : $root; if ($includeNode) { - $phpUnit->assertEquals('Sports', $root['title']); + static::assertSame('Sports', $root['title']); } - $phpUnit->assertEquals('Soccer', $soccer['title']); - $phpUnit->assertEquals('Indoor Soccer', $soccer['__children'][0]['title']); + static::assertSame('Soccer', $soccer['title']); + static::assertSame('Indoor Soccer', $soccer['__children'][0]['title']); } }; // All trees $tree = $repo->childrenHierarchy(null, false, $sortOption); - $testClosure($this, $tree, true, 'both'); + $testClosure($tree, true, 'both'); $roots = $repo->getRootNodes(); @@ -282,7 +338,7 @@ protected function buildTreeTests($class) true ); - $testClosure($this, $tree, true, 'first'); + $testClosure($tree, true, 'first'); // First root tree, not including root node $tree = $repo->childrenHierarchy( @@ -291,7 +347,7 @@ protected function buildTreeTests($class) $sortOption ); - $testClosure($this, $tree, false, 'first'); + $testClosure($tree, false, 'first'); // Second root tree, including root node $tree = $repo->childrenHierarchy( @@ -301,7 +357,7 @@ protected function buildTreeTests($class) true ); - $testClosure($this, $tree, true, 'second'); + $testClosure($tree, true, 'second'); // Second root tree, not including root node $tree = $repo->childrenHierarchy( @@ -310,10 +366,10 @@ protected function buildTreeTests($class) $sortOption ); - $testClosure($this, $tree, false, 'second'); + $testClosure($tree, false, 'second'); - $food = $repo->findOneByTitle('Food'); - $vegitables = $repo->findOneByTitle('Vegitables'); + $food = $repo->findOneBy(['title' => 'Food']); + $vegitables = $repo->findOneBy(['title' => 'Vegitables']); $boringFood = new $class(); $boringFood->setTitle('Boring Food'); @@ -332,7 +388,7 @@ protected function buildTreeTests($class) true ); - $testClosure($this, $tree, true, 'first', true); + $testClosure($tree, true, 'first', true); // First root tree, after inserting a new node in the middle. This not includes the root node $tree = $repo->childrenHierarchy( @@ -341,7 +397,7 @@ protected function buildTreeTests($class) $sortOption ); - $testClosure($this, $tree, false, 'first', true); + $testClosure($tree, false, 'first', true); // Second root tree, after inserting a new node in the middle. This includes the root node $tree = $repo->childrenHierarchy( @@ -351,7 +407,7 @@ protected function buildTreeTests($class) true ); - $testClosure($this, $tree, true, 'second', true); + $testClosure($tree, true, 'second', true); // Second root tree, after inserting a new node in the middle. This not includes the root node $tree = $repo->childrenHierarchy( @@ -360,10 +416,10 @@ protected function buildTreeTests($class) $sortOption ); - $testClosure($this, $tree, false, 'second', false); + $testClosure($tree, false, 'second', false); // Test a subtree, including node - $node = $repo->findOneByTitle('Fruits'); + $node = $repo->findOneBy(['title' => 'Fruits']); $tree = $repo->childrenHierarchy( $node, false, @@ -371,19 +427,19 @@ protected function buildTreeTests($class) true ); - $this->assertEquals('Fruits', $tree[0]['title']); - $this->assertEquals('Berries', $tree[0]['__children'][0]['title']); - $this->assertEquals('Strawberries', $tree[0]['__children'][0]['__children'][0]['title']); + static::assertSame('Fruits', $tree[0]['title']); + static::assertSame('Berries', $tree[0]['__children'][0]['title']); + static::assertSame('Strawberries', $tree[0]['__children'][0]['__children'][0]['title']); - $node = $repo->findOneByTitle('Fruits'); + $node = $repo->findOneBy(['title' => 'Fruits']); $tree = $repo->childrenHierarchy( $node, false, $sortOption ); - $this->assertEquals('Berries', $tree[0]['title']); - $this->assertEquals('Strawberries', $tree[0]['__children'][0]['title']); + static::assertSame('Berries', $tree[0]['title']); + static::assertSame('Strawberries', $tree[0]['__children'][0]['title']); // First Tree Direct Nodes, including root node $tree = $repo->childrenHierarchy( @@ -394,11 +450,11 @@ protected function buildTreeTests($class) ); $food = $tree[0]; - $this->assertEquals('Food', $food['title']); - $this->assertEquals(3, count($food['__children'])); - $this->assertEquals('Boring Food', $food['__children'][0]['title']); - $this->assertEquals('Fruits', $food['__children'][1]['title']); - $this->assertEquals('Milk', $food['__children'][2]['title']); + static::assertSame('Food', $food['title']); + static::assertCount(3, $food['__children']); + static::assertSame('Boring Food', $food['__children'][0]['title']); + static::assertSame('Fruits', $food['__children'][1]['title']); + static::assertSame('Milk', $food['__children'][2]['title']); // First Tree Direct Nodes, not including root node $tree = $repo->childrenHierarchy( @@ -407,47 +463,45 @@ protected function buildTreeTests($class) $sortOption ); - $this->assertEquals(3, count($tree)); - $this->assertEquals('Boring Food', $tree[0]['title']); - $this->assertEquals('Fruits', $tree[1]['title']); - $this->assertEquals('Milk', $tree[2]['title']); + static::assertCount(3, $tree); + static::assertSame('Boring Food', $tree[0]['title']); + static::assertSame('Fruits', $tree[1]['title']); + static::assertSame('Milk', $tree[2]['title']); // Helper Closures - $getTree = function ($includeNode) use ($repo, $roots, $sortOption) { - return $repo->childrenHierarchy( - $roots[0], - true, - array_merge($sortOption, array('decorate' => true)), - $includeNode - ); - }; - $getTreeHtml = function ($includeNode) { + $getTree = static fn ($includeNode) => $repo->childrenHierarchy( + $roots[0], + true, + array_merge($sortOption, ['decorate' => true]), + $includeNode + ); + $getTreeHtml = static function ($includeNode) { $baseHtml = '
      • Boring Food
        • Vegitables
          • Cabbages
          • Carrots
      • Fruits
        • Berries
          • Strawberries
        • Lemons
        • Oranges
      • Milk
        • Cheese
          • Mould cheese
      • '; return $includeNode ? '
        • Food
            '.$baseHtml.'
          ' : '
            '.$baseHtml; }; // First Tree - Including Root Node - Html test - $this->assertEquals($getTreeHtml(true), $getTree(true)); + static::assertSame($getTreeHtml(true), $getTree(true)); // First Tree - Not including Root Node - Html test - $this->assertEquals($getTreeHtml(false), $getTree(false)); + static::assertSame($getTreeHtml(false), $getTree(false)); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CATEGORY, - self::CLOSURE, - self::CATEGORY_WITHOUT_LEVEL, - self::CATEGORY_WITHOUT_LEVEL_CLOSURE, - ); + return [ + Category::class, + CategoryClosure::class, + CategoryWithoutLevel::class, + CategoryWithoutLevelClosure::class, + ]; } - private function populate($class = self::CATEGORY) + private function populate(string $class = Category::class): void { $food = new $class(); - $food->setTitle("Food"); + $food->setTitle('Food'); $this->em->persist($food); $vegitables = new $class(); diff --git a/tests/Gedmo/Tree/ClosureTreeTest.php b/tests/Gedmo/Tree/ClosureTreeTest.php index 3dc367b995..8da031620c 100644 --- a/tests/Gedmo/Tree/ClosureTreeTest.php +++ b/tests/Gedmo/Tree/ClosureTreeTest.php @@ -1,37 +1,45 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\Closure\Category; -use Tree\Fixture\Closure\News; -use Tree\Fixture\Closure\CategoryClosure; -use Tree\Fixture\Closure\CategoryWithoutLevel; -use Tree\Fixture\Closure\CategoryWithoutLevelClosure; +use Gedmo\Exception\UnexpectedValueException; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Closure\Category; +use Gedmo\Tests\Tree\Fixture\Closure\CategoryClosure; +use Gedmo\Tests\Tree\Fixture\Closure\CategoryWithoutLevel; +use Gedmo\Tests\Tree\Fixture\Closure\CategoryWithoutLevelClosure; +use Gedmo\Tests\Tree\Fixture\Closure\News; +use Gedmo\Tests\Tree\Fixture\Closure\Person; +use Gedmo\Tests\Tree\Fixture\Closure\PersonClosure; +use Gedmo\Tests\Tree\Fixture\Closure\User; +use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; +use Gedmo\Tree\Strategy\ORM\Closure; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gustavo Adrian * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ClosureTreeTest extends BaseTestCaseORM +final class ClosureTreeTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\Closure\\Category"; - const CLOSURE = "Tree\\Fixture\\Closure\\CategoryClosure"; - const PERSON = "Tree\\Fixture\\Closure\\Person"; - const USER = "Tree\\Fixture\\Closure\\User"; - const PERSON_CLOSURE = "Tree\\Fixture\\Closure\\PersonClosure"; - const NEWS = "Tree\\Fixture\\Closure\\News"; - const CATEGORY_WITHOUT_LEVEL = "Tree\\Fixture\\Closure\\CategoryWithoutLevel"; - const CATEGORY_WITHOUT_LEVEL_CLOSURE = "Tree\\Fixture\\Closure\\CategoryWithoutLevelClosure"; - + /** + * @var TreeListener + */ protected $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -40,7 +48,7 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($this->listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } @@ -52,7 +60,7 @@ protected function setUp() $minutes = intval($took / 60); $seconds = $took % 60; echo sprintf("%s --> %02d:%02d", $msg, $minutes, $seconds) . PHP_EOL; }; - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(Category::class); $parent = null; $num = 800; for($i = 0; $i < 500; $i++) { @@ -75,35 +83,35 @@ protected function setUp() $dumpTime($start, $num.' - inserts took:'); $start = microtime(true); // test moving - $target = $repo->findOneByTitle('cat300'); - $dest = $repo->findOneByTitle('cat2000'); + $target = $repo->findOneBy(['title' => 'cat300']); + $dest = $repo->findOneBy(['title' => 'cat2000']); $target->setParent($dest); - $target2 = $repo->findOneByTitle('cat450'); - $dest2 = $repo->findOneByTitle('cat2500'); + $target2 = $repo->findOneBy(['title' => 'cat450']); + $dest2 = $repo->findOneBy(['title' => 'cat2500']); $target2->setParent($dest2); $this->em->flush(); $dumpTime($start, 'moving took:'); }*/ - public function testClosureTree() + public function testClosureTree(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $closureRepo = $this->em->getRepository(self::CLOSURE); + $repo = $this->em->getRepository(Category::class); - $food = $repo->findOneByTitle('Food'); - $dql = 'SELECT c FROM '.self::CLOSURE.' c'; + $food = $repo->findOneBy(['title' => 'Food']); + $dql = 'SELECT c FROM '.CategoryClosure::class.' c'; $dql .= ' WHERE c.ancestor = :ancestor'; $query = $this->em->createQuery($dql); $query->setParameter('ancestor', $food); $foodClosures = $query->getResult(); - $this->assertCount(12, $foodClosures); + static::assertCount(12, $foodClosures); foreach ($foodClosures as $closure) { $descendant = $closure->getDescendant(); if ($descendant === $food) { - $this->assertEquals(0, $closure->getDepth()); + static::assertSame(0, $closure->getDepth()); + continue; } $descendantTitle = $descendant->getTitle(); @@ -111,121 +119,130 @@ public function testClosureTree() $descendantClosures = $query->getResult(); switch ($descendantTitle) { case 'Fruits': - $this->assertCount(5, $descendantClosures); - $this->assertEquals(1, $closure->getDepth()); + static::assertCount(5, $descendantClosures); + static::assertSame(1, $closure->getDepth()); + break; case 'Oranges': - $this->assertCount(1, $descendantClosures); - $this->assertEquals(2, $closure->getDepth()); + static::assertCount(1, $descendantClosures); + static::assertSame(2, $closure->getDepth()); + break; case 'Berries': - $this->assertCount(2, $descendantClosures); - $this->assertEquals(2, $closure->getDepth()); + static::assertCount(2, $descendantClosures); + static::assertSame(2, $closure->getDepth()); + break; case 'Vegitables': - $this->assertCount(3, $descendantClosures); - $this->assertEquals(1, $closure->getDepth()); + static::assertCount(3, $descendantClosures); + static::assertSame(1, $closure->getDepth()); + break; case 'Milk': - $this->assertCount(3, $descendantClosures); - $this->assertEquals(1, $closure->getDepth()); + static::assertCount(3, $descendantClosures); + static::assertSame(1, $closure->getDepth()); + break; case 'Cheese': - $this->assertCount(2, $descendantClosures); - $this->assertEquals(2, $closure->getDepth()); + static::assertCount(2, $descendantClosures); + static::assertSame(2, $closure->getDepth()); + break; case 'Strawberries': - $this->assertCount(1, $descendantClosures); - $this->assertEquals(3, $closure->getDepth()); + static::assertCount(1, $descendantClosures); + static::assertSame(3, $closure->getDepth()); + break; } } } - public function testUpdateOfParent() + public function testUpdateOfParent(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $strawberries = $repo->findOneByTitle('Strawberries'); - $cheese = $repo->findOneByTitle('Cheese'); + $repo = $this->em->getRepository(Category::class); + $strawberries = $repo->findOneBy(['title' => 'Strawberries']); + $cheese = $repo->findOneBy(['title' => 'Cheese']); $strawberries->setParent($cheese); $this->em->persist($strawberries); $this->em->flush(); - $dql = 'SELECT c FROM '.self::CLOSURE.' c'; + $dql = 'SELECT c FROM '.CategoryClosure::class.' c'; $dql .= ' WHERE c.descendant = :descendant'; $query = $this->em->createQuery($dql); $query->setParameter('descendant', $strawberries); $closures = $query->getResult(); - $this->assertTrue($this->hasAncestor($closures, 'Cheese')); - $this->assertTrue($this->hasAncestor($closures, 'Milk')); - $this->assertTrue($this->hasAncestor($closures, 'Food')); - $this->assertFalse($this->hasAncestor($closures, 'Berries')); - $this->assertFalse($this->hasAncestor($closures, 'Fruits')); + static::assertTrue($this->hasAncestor($closures, 'Cheese')); + static::assertTrue($this->hasAncestor($closures, 'Milk')); + static::assertTrue($this->hasAncestor($closures, 'Food')); + static::assertFalse($this->hasAncestor($closures, 'Berries')); + static::assertFalse($this->hasAncestor($closures, 'Fruits')); } - public function testAnotherUpdateOfParent() + public function testAnotherUpdateOfParent(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $strawberries = $repo->findOneByTitle('Strawberries'); + $repo = $this->em->getRepository(Category::class); + $strawberries = $repo->findOneBy(['title' => 'Strawberries']); $strawberries->setParent(null); $this->em->persist($strawberries); $this->em->flush(); - $dql = 'SELECT c FROM '.self::CLOSURE.' c'; + $dql = 'SELECT c FROM '.CategoryClosure::class.' c'; $dql .= ' WHERE c.descendant = :descendant'; $query = $this->em->createQuery($dql); $query->setParameter('descendant', $strawberries); $closures = $query->getResult(); - $this->assertCount(1, $closures); - $this->assertTrue($this->hasAncestor($closures, 'Strawberries')); + static::assertCount(1, $closures); + static::assertTrue($this->hasAncestor($closures, 'Strawberries')); } - public function testBranchRemoval() + public function testBranchRemoval(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $fruits = $repo->findOneByTitle('Fruits'); + $repo = $this->em->getRepository(Category::class); + $fruits = $repo->findOneBy(['title' => 'Fruits']); $id = $fruits->getId(); $this->em->remove($fruits); $this->em->flush(); - $dql = 'SELECT COUNT(c) FROM '.self::CLOSURE.' c'; + $dql = 'SELECT COUNT(c) FROM '.CategoryClosure::class.' c'; $dql .= ' JOIN c.descendant d'; $dql .= ' JOIN c.ancestor a'; $dql .= ' WHERE (a.id = :id OR d.id = :id)'; $query = $this->em->createQuery($dql); $query->setParameter('id', $id); - $this->assertEquals(0, $query->getSingleScalarResult()); + static::assertSame(0, (int) $query->getSingleScalarResult()); // pdo_sqlite will not cascade } - /** - * @expectedException Gedmo\Exception\UnexpectedValueException - */ - public function testSettingParentToChild() + public function testSettingParentToChild(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $fruits = $repo->findOneByTitle('Fruits'); - $strawberries = $repo->findOneByTitle('Strawberries'); + $this->expectException(UnexpectedValueException::class); + $repo = $this->em->getRepository(Category::class); + $fruits = $repo->findOneBy(['title' => 'Fruits']); + $strawberries = $repo->findOneBy(['title' => 'Strawberries']); $fruits->setParent($strawberries); $this->em->flush(); } - public function testIfEntityHasNotIncludedTreeLevelFieldThenDontProcessIt() + public function testIfEntityHasNotIncludedTreeLevelFieldThenDontProcessIt(): void { - $listener = $this->getMock('Gedmo\Tree\TreeListener', array('getStrategy')); - $strategy = $this->getMock('Gedmo\Tree\Strategy\ORM\Closure', array('setLevelFieldOnPendingNodes'), array($listener)); - $listener->expects($this->any()) + $listener = $this->getMockBuilder(TreeListener::class)->getMock(); + $strategy = $this->getMockBuilder(Closure::class) + ->setMethods(['setLevelFieldOnPendingNodes']) + ->setConstructorArgs([$listener]) + ->getMock(); + + $listener ->method('getStrategy') - ->will($this->returnValue($strategy)); + ->willReturn($strategy); - $strategy->expects($this->never()) + $strategy->expects(static::never()) ->method('setLevelFieldOnPendingNodes'); $evm = $this->em->getEventManager(); @@ -240,38 +257,132 @@ public function testIfEntityHasNotIncludedTreeLevelFieldThenDontProcessIt() $this->em->flush(); } - private function hasAncestor($closures, $name) + public function testCascadePersistTree(): void + { + $politics = new Category(); + $politics->setTitle('Politics'); + + $news = new News('Lorem ipsum', $politics); + $this->em->persist($news); + $this->em->flush(); + + $closure = $this->em->createQueryBuilder() + ->select('c') + ->from(CategoryClosure::class, 'c') + ->where('c.ancestor = :ancestor') + ->setParameter('ancestor', $politics->getId()) + ->getQuery() + ->getResult(); + + static::assertCount(1, $closure); + } + + public function testPersistOnRightEmInstance(): void + { + $evm = new EventManager(); + $evm->addEventSubscriber(new TreeListener()); + + $emOne = $this->getDefaultMockSqliteEntityManager($evm); + $emTwo = $this->getDefaultMockSqliteEntityManager($evm); + + $categoryOne = new Category(); + $categoryOne->setTitle('Politics'); + + $categoryTwo = new Category(); + $categoryTwo->setTitle('Politics'); + + // Persist and Flush on different times ! + $emOne->persist($categoryOne); + + $emTwo->persist($categoryTwo); + $emTwo->flush(); + + $emOne->flush(); + + static::assertNotNull($categoryOne->getId()); + static::assertNotNull($categoryTwo->getId()); + } + + /** + * @dataProvider provideNodeOrders + */ + public function testClosuresCreatedMustNotBeAffectedByPersistOrder(Category $firstToPersist, Category $secondToPersist, Category $thirdToPersist): void + { + $evm = new EventManager(); + $evm->addEventSubscriber($this->listener); + + $this->getDefaultMockSqliteEntityManager($evm); + + $this->em->persist($firstToPersist); + $this->em->persist($secondToPersist); + $this->em->persist($thirdToPersist); + $this->em->flush(); + $this->em->clear(); + + $closures = $this->em->getRepository(CategoryClosure::class)->findAll(); + + static::assertCount(6, $closures); + } + + /** + * @return array> + */ + public static function provideNodeOrders(): array + { + $grandpa = new Category(); + $grandpa->setTitle('grandpa'); + + $father = new Category(); + $father->setTitle('father'); + $father->setParent($grandpa); + + $son = new Category(); + $son->setTitle('son'); + $son->setParent($father); + + return [ + 'order-123' => [$grandpa, $father, $son], + 'order-132' => [$grandpa, $son, $father], + 'order-213' => [$father, $grandpa, $son], + 'order-231' => [$father, $son, $grandpa], + 'order-312' => [$son, $grandpa, $father], + 'order-321' => [$son, $father, $grandpa], + ]; + } + + protected function getUsedEntityFixtures(): array + { + return [ + Category::class, + CategoryClosure::class, + Person::class, + PersonClosure::class, + User::class, + News::class, + CategoryWithoutLevel::class, + CategoryWithoutLevelClosure::class, + ]; + } + + /** + * @param iterable $closures + */ + private function hasAncestor(iterable $closures, string $name): bool { - $result = false; foreach ($closures as $closure) { $ancestor = $closure->getAncestor(); if ($ancestor->getTitle() === $name) { - $result = true; - break; + return true; } } - return $result; - } - - protected function getUsedEntityFixtures() - { - return array( - self::CATEGORY, - self::CLOSURE, - self::PERSON, - self::PERSON_CLOSURE, - self::USER, - self::NEWS, - self::CATEGORY_WITHOUT_LEVEL, - self::CATEGORY_WITHOUT_LEVEL_CLOSURE, - ); + return false; } - private function populate() + private function populate(): void { $food = new Category(); - $food->setTitle("Food"); + $food->setTitle('Food'); $this->em->persist($food); $fruits = new Category(); @@ -331,24 +442,4 @@ private function populate() $this->em->flush(); } - - public function testCascadePersistTree() - { - $politics = new Category(); - $politics->setTitle('Politics'); - - $news = new News('Lorem ipsum', $politics); - $this->em->persist($news); - $this->em->flush(); - - $closure = $this->em->createQueryBuilder() - ->select('c') - ->from(self::CLOSURE, 'c') - ->where('c.ancestor = :ancestor') - ->setParameter('ancestor', $politics->getId()) - ->getQuery() - ->getResult(); - - $this->assertCount(1, $closure); - } } diff --git a/tests/Gedmo/Tree/ConcurrencyTest.php b/tests/Gedmo/Tree/ConcurrencyTest.php index f60affedab..8c18a8fe4e 100644 --- a/tests/Gedmo/Tree/ConcurrencyTest.php +++ b/tests/Gedmo/Tree/ConcurrencyTest.php @@ -1,41 +1,45 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\Category; -use Tree\Fixture\Article; -use Tree\Fixture\Comment; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Article; +use Gedmo\Tests\Tree\Fixture\Category; +use Gedmo\Tests\Tree\Fixture\Comment; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class ConcurrencyTest extends BaseTestCaseORM +final class ConcurrencyTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\Category"; - const ARTICLE = "Tree\\Fixture\\Article"; - const COMMENT = "Tree\\Fixture\\Comment"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testConcurrentEntitiesInOneFlush() + public function testConcurrentEntitiesInOneFlush(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $sport = $repo->findOneByTitle('Root2'); + $repo = $this->em->getRepository(Category::class); + $sport = $repo->findOneBy(['title' => 'Root2']); $sport->setTitle('Sport'); $skiing = new Category(); @@ -72,75 +76,76 @@ public function testConcurrentEntitiesInOneFlush() $this->em->flush(); $this->em->clear(); - $meta = $this->em->getClassMetadata(self::CATEGORY); - $sport = $repo->findOneByTitle('Sport'); + $meta = $this->em->getClassMetadata(Category::class); + $sport = $repo->findOneBy(['title' => 'Sport']); $left = $meta->getReflectionProperty('lft')->getValue($sport); $right = $meta->getReflectionProperty('rgt')->getValue($sport); - $this->assertEquals(9, $left); - $this->assertEquals(16, $right); + static::assertSame(9, $left); + static::assertSame(16, $right); - $skiing = $repo->findOneByTitle('Skiing'); + $skiing = $repo->findOneBy(['title' => 'Skiing']); $left = $meta->getReflectionProperty('lft')->getValue($skiing); $right = $meta->getReflectionProperty('rgt')->getValue($skiing); - $this->assertEquals(10, $left); - $this->assertEquals(13, $right); + static::assertSame(10, $left); + static::assertSame(13, $right); } - public function testConcurrentTree() + public function testConcurrentTree(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $meta = $this->em->getClassMetadata(self::CATEGORY); + $repo = $this->em->getRepository(Category::class); + // Force metadata class loading. + $this->em->getClassMetadata(Category::class); - $root = $repo->findOneByTitle('Root'); + $root = $repo->findOneBy(['title' => 'Root']); - $this->assertEquals(1, $root->getLeft()); - $this->assertEquals(8, $root->getRight()); + static::assertSame(1, $root->getLeft()); + static::assertSame(8, $root->getRight()); - $root2 = $repo->findOneByTitle('Root2'); + $root2 = $repo->findOneBy(['title' => 'Root2']); - $this->assertEquals(9, $root2->getLeft()); - $this->assertEquals(10, $root2->getRight()); + static::assertSame(9, $root2->getLeft()); + static::assertSame(10, $root2->getRight()); - $child2Child = $repo->findOneByTitle('childs2_child'); + $child2Child = $repo->findOneBy(['title' => 'childs2_child']); - $this->assertEquals(5, $child2Child->getLeft()); - $this->assertEquals(6, $child2Child->getRight()); + static::assertSame(5, $child2Child->getLeft()); + static::assertSame(6, $child2Child->getRight()); $child2Parent = $child2Child->getParent(); - $this->assertEquals(4, $child2Parent->getLeft()); - $this->assertEquals(7, $child2Parent->getRight()); + static::assertSame(4, $child2Parent->getLeft()); + static::assertSame(7, $child2Parent->getRight()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CATEGORY, - self::ARTICLE, - self::COMMENT, - ); + return [ + Category::class, + Article::class, + Comment::class, + ]; } - private function populate() + private function populate(): void { $root = new Category(); - $root->setTitle("Root"); + $root->setTitle('Root'); $root2 = new Category(); - $root2->setTitle("Root2"); + $root2->setTitle('Root2'); $child = new Category(); - $child->setTitle("child"); + $child->setTitle('child'); $child->setParent($root); $child2 = new Category(); - $child2->setTitle("child2"); + $child2->setTitle('child2'); $child2->setParent($root); $childsChild = new Category(); - $childsChild->setTitle("childs2_child"); + $childsChild->setTitle('childs2_child'); $childsChild->setParent($child2); $this->em->persist($root); diff --git a/tests/Gedmo/Tree/Fixture/ANode.php b/tests/Gedmo/Tree/Fixture/ANode.php index e7cfeb9d1e..05789a9ca0 100644 --- a/tests/Gedmo/Tree/Fixture/ANode.php +++ b/tests/Gedmo/Tree/Fixture/ANode.php @@ -1,52 +1,82 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\MappedSuperclass */ +#[ORM\MappedSuperclass] class ANode { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** + * @var int|null + * * @Gedmo\TreeLeft + * * @ORM\Column(type="integer", nullable=true) */ + #[ORM\Column(type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLeft] private $lft; /** + * @var int|null + * * @Gedmo\TreeRight + * * @ORM\Column(type="integer", nullable=true) */ + #[ORM\Column(type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeRight] private $rgt; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="BaseNode", inversedBy="children") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") */ - private $parent; + #[ORM\ManyToOne(targetEntity: BaseNode::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?BaseNode $parent = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setParent($parent = null) + public function setParent(?BaseNode $parent = null): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?BaseNode { return $this->parent; } diff --git a/tests/Gedmo/Tree/Fixture/Article.php b/tests/Gedmo/Tree/Fixture/Article.php index ca054eb441..626b55bd1b 100644 --- a/tests/Gedmo/Tree/Fixture/Article.php +++ b/tests/Gedmo/Tree/Fixture/Article.php @@ -1,62 +1,94 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=128) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + private ?string $title = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Comment", mappedBy="article") */ + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article')] private $comments; /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="articles") */ - private $category; + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'articles')] + private ?Category $category = null; - public function getId() + public function __construct() + { + $this->comments = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function setCategory($category) + public function setCategory(?Category $category): void { $this->category = $category; } - public function addComment(Comment $comment) + public function addComment(Comment $comment): void { $comment->setArticle($this); $this->comments[] = $comment; } - public function getComments() + /** + * @return Collection + */ + public function getComments(): Collection { return $this->comments; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Tree/Fixture/BaseNode.php b/tests/Gedmo/Tree/Fixture/BaseNode.php index 7042b5acd0..7b9bbac406 100644 --- a/tests/Gedmo/Tree/Fixture/BaseNode.php +++ b/tests/Gedmo/Tree/Fixture/BaseNode.php @@ -1,62 +1,103 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") * @ORM\InheritanceType("SINGLE_TABLE") * @ORM\DiscriminatorColumn(name="discriminator", type="string") - * @ORM\DiscriminatorMap({"base" = "BaseNode", "node" = "Node"}) + * @ORM\DiscriminatorMap({"base": "BaseNode", "node": "Node"}) + * * @Gedmo\Tree(type="nested") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[ORM\InheritanceType('SINGLE_TABLE')] +#[ORM\DiscriminatorColumn(name: 'discriminator', type: Types::STRING)] +#[ORM\DiscriminatorMap(['base' => BaseNode::class, 'node' => Node::class])] +#[Gedmo\Tree(type: 'nested')] class BaseNode extends ANode { /** + * @var Collection + * * @ORM\OneToMany(targetEntity="BaseNode", mappedBy="parent") */ - private $children; + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; /** + * @var \DateTime|null + * * @Gedmo\Timestampable(on="create") + * * @ORM\Column(type="datetime") */ + #[ORM\Column(type: Types::DATETIME_MUTABLE)] + #[Gedmo\Timestampable(on: 'create')] private $created; /** * @ORM\Column(length=128, unique=true) */ - private $identifier; + #[ORM\Column(length: 128, unique: true)] + private ?string $identifier = null; /** + * @var \DateTime|null + * * @ORM\Column(type="datetime") + * * @Gedmo\Timestampable */ + #[ORM\Column(type: Types::DATETIME_MUTABLE)] + #[Gedmo\Timestampable] private $updated; - public function getCreated() + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getCreated(): ?\DateTime { return $this->created; } - public function getUpdated() + public function getUpdated(): ?\DateTime { return $this->updated; } - public function getChildren() + /** + * @return Collection + */ + public function getChildren(): Collection { return $this->children; } - public function getIdentifier() + public function getIdentifier(): ?string { return $this->identifier; } - public function setIdentifier($identifier) + public function setIdentifier(?string $identifier): void { $this->identifier = $identifier; } diff --git a/tests/Gedmo/Tree/Fixture/BehavioralCategory.php b/tests/Gedmo/Tree/Fixture/BehavioralCategory.php index c0d18548d7..4e927a25b0 100644 --- a/tests/Gedmo/Tree/Fixture/BehavioralCategory.php +++ b/tests/Gedmo/Tree/Fixture/BehavioralCategory.php @@ -1,88 +1,140 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tests\Tree\Fixture\Repository\BehavioralCategoryRepository; /** - * @ORM\Entity(repositoryClass="Tree\Fixture\Repository\BehavioralCategoryRepository") + * @ORM\Entity(repositoryClass="Gedmo\Tests\Tree\Fixture\Repository\BehavioralCategoryRepository") + * * @Gedmo\Tree(type="nested") */ +#[ORM\Entity(repositoryClass: BehavioralCategoryRepository::class)] +#[Gedmo\Tree(type: 'nested')] class BehavioralCategory { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + #[Gedmo\Translatable] + private ?string $title = null; /** + * @var int|null + * * @Gedmo\TreeLeft + * * @ORM\Column(name="lft", type="integer", nullable=true) */ + #[ORM\Column(name: 'lft', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLeft] private $lft; /** + * @var int|null + * * @Gedmo\TreeRight + * * @ORM\Column(name="rgt", type="integer", nullable=true) */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeRight] private $rgt; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="BehavioralCategory", inversedBy="children") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * }) */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?BehavioralCategory $parent = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="BehavioralCategory", mappedBy="parent") */ - private $children; + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; /** + * @var string|null + * * @Gedmo\Translatable * @Gedmo\Slug(fields={"title"}) + * * @ORM\Column(name="slug", type="string", length=128, unique=true) */ + #[ORM\Column(name: 'slug', type: Types::STRING, length: 128, unique: true)] + #[Gedmo\Translatable] + #[Gedmo\Slug(fields: ['title'])] private $slug; - public function getId() + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(BehavioralCategory $parent) + public function setParent(self $parent): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } diff --git a/tests/Gedmo/Tree/Fixture/Category.php b/tests/Gedmo/Tree/Fixture/Category.php index e9dcdb475f..0f81b41999 100644 --- a/tests/Gedmo/Tree/Fixture/Category.php +++ b/tests/Gedmo/Tree/Fixture/Category.php @@ -1,103 +1,168 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; +use Gedmo\Tree\Node as NodeInterface; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") + * * @Gedmo\Tree(type="nested") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[Gedmo\Tree(type: 'nested')] class Category implements NodeInterface { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** + * @var int|null + * * @Gedmo\TreeLeft + * * @ORM\Column(name="lft", type="integer") */ + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] private $lft; /** + * @var int|null + * * @Gedmo\TreeRight + * * @ORM\Column(name="rgt", type="integer") */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] private $rgt; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="Category", inversedBy="children") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * }) */ - private $parentId; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?Category $parentId = null; /** + * @var int|null + * * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer") */ - private $level; + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private $level; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Category", mappedBy="parent") */ - private $children; + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="category") */ - private $comments; + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'article')] + private Collection $comments; - public function getId() + private ?NodeInterface $sibling = null; + + public function __construct() + { + $this->children = new ArrayCollection(); + $this->comments = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(Category $parent) + public function setParent(self $parent): void { $this->parentId = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parentId; } - public function getLeft() + public function getLeft(): ?int { return $this->lft; } - public function getRight() + public function getRight(): ?int { return $this->rgt; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } + + public function setSibling(NodeInterface $node): void + { + $this->sibling = $node; + } + + public function getSibling(): ?NodeInterface + { + return $this->sibling; + } } diff --git a/tests/Gedmo/Tree/Fixture/CategoryUuid.php b/tests/Gedmo/Tree/Fixture/CategoryUuid.php index 9bcb94f517..5526add92e 100644 --- a/tests/Gedmo/Tree/Fixture/CategoryUuid.php +++ b/tests/Gedmo/Tree/Fixture/CategoryUuid.php @@ -1,16 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; +use Gedmo\Tree\Node as NodeInterface; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") + * * @Gedmo\Tree(type="nested") + * * @ORM\HasLifecycleCallbacks */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[ORM\HasLifecycleCallbacks] +#[Gedmo\Tree(type: 'nested')] class CategoryUuid implements NodeInterface { /** @@ -18,104 +36,156 @@ class CategoryUuid implements NodeInterface * @ORM\Id * @ORM\GeneratedValue(strategy="NONE") */ - private $id; + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'NONE')] + #[ORM\Column(name: 'id', type: Types::STRING, nullable: false)] + private ?string $id = null; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** + * @var int|null + * * @Gedmo\TreeLeft + * * @ORM\Column(name="lft", type="integer") */ + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] private $lft; /** + * @var int|null + * * @Gedmo\TreeRight + * * @ORM\Column(name="rgt", type="integer") */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] private $rgt; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="CategoryUuid", inversedBy="children") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * }) */ - private $parentId; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?CategoryUuid $parentId = null; /** + * @var int|null + * * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer") */ - private $level; + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private $level; /** + * @var string|null + * * @Gedmo\TreeRoot + * * @ORM\Column(name="root", type="string") */ - private $root; + #[ORM\Column(name: 'root', type: Types::STRING)] + #[Gedmo\TreeRoot] + private $root; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="CategoryUuid", mappedBy="parent") */ - private $children; + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="category") */ - private $comments; + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')] + private Collection $comments; + + private ?NodeInterface $sibling = null; + + public function __construct() + { + $this->children = new ArrayCollection(); + $this->comments = new ArrayCollection(); + } /** * Creates a random uuid on persist * - * @return void * @ORM\PrePersist */ - public function createId() + #[ORM\PrePersist] + public function createId(): void { - $this->id = bin2hex(pack('N2', mt_rand(), mt_rand())); + $this->id = bin2hex(pack('N2', random_int(0, mt_getrandmax()), random_int(0, mt_getrandmax()))); } - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(CategoryUuid $parent) + public function setParent(self $parent): void { $this->parentId = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parentId; } - public function getLeft() + public function getLeft(): ?int { return $this->lft; } - public function getRight() + public function getRight(): ?int { return $this->rgt; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } + + public function setSibling(NodeInterface $node): void + { + $this->sibling = $node; + } + + public function getSibling(): ?NodeInterface + { + return $this->sibling; + } } diff --git a/tests/Gedmo/Tree/Fixture/Closure/Category.php b/tests/Gedmo/Tree/Fixture/Closure/Category.php index 19a40186cd..89c54fc77f 100644 --- a/tests/Gedmo/Tree/Fixture/Closure/Category.php +++ b/tests/Gedmo/Tree/Fixture/Closure/Category.php @@ -1,78 +1,118 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Closure; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\ClosureTreeRepository; /** * @Gedmo\Tree(type="closure") * @Gedmo\TreeClosure(class="CategoryClosure") + * * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\ClosureTreeRepository") */ +#[ORM\Entity(repositoryClass: ClosureTreeRepository::class)] +#[Gedmo\Tree(type: 'closure')] +#[Gedmo\TreeClosure(class: CategoryClosure::class)] class Category { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @ORM\Column(name="level", type="integer", nullable=true) + * * @Gedmo\TreeLevel */ - private $level; + #[ORM\Column(name: 'level', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLevel] + private ?int $level = null; /** * @Gedmo\TreeParent + * * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * @ORM\ManyToOne(targetEntity="Category", inversedBy="children") */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'category_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?Category $parent = null; + + /** + * @var Collection + */ + private $closures; + + public function __construct() + { + $this->closures = new ArrayCollection(); + } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(Category $parent = null) + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function addClosure(CategoryClosure $closure) + public function addClosure(CategoryClosure $closure): void { $this->closures[] = $closure; } - public function setLevel($level) + public function setLevel(?int $level): void { $this->level = $level; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } diff --git a/tests/Gedmo/Tree/Fixture/Closure/CategoryClosure.php b/tests/Gedmo/Tree/Fixture/Closure/CategoryClosure.php index 52e50e2275..a8000e50d2 100644 --- a/tests/Gedmo/Tree/Fixture/Closure/CategoryClosure.php +++ b/tests/Gedmo/Tree/Fixture/Closure/CategoryClosure.php @@ -1,13 +1,46 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Closure; -use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; /** * @ORM\Entity + * @ORM\Table( + * indexes={@ORM\Index(name="closure_category_depth_idx", columns={"depth"})}, + * uniqueConstraints={@ORM\UniqueConstraint(name="closure_category_unique_idx", columns={ + * "ancestor", "descendant" + * })} + * ) */ +#[ORM\Entity] +#[ORM\UniqueConstraint(name: 'closure_category_unique_idx', columns: ['ancestor', 'descendant'])] +#[ORM\Index(name: 'closure_category_depth_idx', columns: ['depth'])] class CategoryClosure extends AbstractClosure { + /** + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\Category") + * @ORM\JoinColumn(name="ancestor", referencedColumnName="id", nullable=false, onDelete="CASCADE") + */ + #[ORM\ManyToOne(targetEntity: Category::class)] + #[ORM\JoinColumn(name: 'ancestor', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected $ancestor; + + /** + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\Category") + * @ORM\JoinColumn(name="descendant", referencedColumnName="id", nullable=false, onDelete="CASCADE") + */ + #[ORM\ManyToOne(targetEntity: Category::class)] + #[ORM\JoinColumn(name: 'descendant', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected $descendant; } diff --git a/tests/Gedmo/Tree/Fixture/Closure/CategoryClosureWithoutMapping.php b/tests/Gedmo/Tree/Fixture/Closure/CategoryClosureWithoutMapping.php new file mode 100644 index 0000000000..065a83f9e0 --- /dev/null +++ b/tests/Gedmo/Tree/Fixture/Closure/CategoryClosureWithoutMapping.php @@ -0,0 +1,23 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Closure; + +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class CategoryClosureWithoutMapping extends AbstractClosure +{ +} diff --git a/tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevel.php b/tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevel.php index 653c2a0aa8..e4e57a8b28 100644 --- a/tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevel.php +++ b/tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevel.php @@ -1,62 +1,99 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Closure; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\ClosureTreeRepository; /** * @Gedmo\Tree(type="closure") - * @Gedmo\TreeClosure(class="Tree\Fixture\Closure\CategoryWithoutLevelClosure") + * @Gedmo\TreeClosure(class="Gedmo\Tests\Tree\Fixture\Closure\CategoryWithoutLevelClosure") + * * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\ClosureTreeRepository") */ +#[ORM\Entity(repositoryClass: ClosureTreeRepository::class)] +#[Gedmo\Tree(type: 'closure')] +#[Gedmo\TreeClosure(class: CategoryWithoutLevelClosure::class)] class CategoryWithoutLevel { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** * @Gedmo\TreeParent + * * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * @ORM\ManyToOne(targetEntity="CategoryWithoutLevel", inversedBy="children") */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'category_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?CategoryWithoutLevel $parent = null; + + /** + * @var Collection + */ + private $closures; + + public function __construct() + { + $this->closures = new ArrayCollection(); + } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(CategoryWithoutLevel $parent = null) + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function addClosure(CategoryWithoutLevelClosure $closure) + public function addClosure(CategoryWithoutLevelClosure $closure): void { $this->closures[] = $closure; } diff --git a/tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevelClosure.php b/tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevelClosure.php index b8205dbfc0..7f136b4ae6 100644 --- a/tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevelClosure.php +++ b/tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevelClosure.php @@ -1,13 +1,46 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Closure; -use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; /** * @ORM\Entity + * @ORM\Table( + * indexes={@ORM\Index(name="closure_category_without_level_depth_idx", columns={"depth"})}, + * uniqueConstraints={@ORM\UniqueConstraint(name="closure_category_without_level_unique_idx", columns={ + * "ancestor", "descendant" + * })} + * ) */ +#[ORM\Entity] +#[ORM\UniqueConstraint(name: 'closure_category_without_level_unique_idx', columns: ['ancestor', 'descendant'])] +#[ORM\Index(name: 'closure_category_without_level_depth_idx', columns: ['depth'])] class CategoryWithoutLevelClosure extends AbstractClosure { + /** + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\CategoryWithoutLevel") + * @ORM\JoinColumn(name="ancestor", referencedColumnName="id", nullable=false, onDelete="CASCADE") + */ + #[ORM\ManyToOne(targetEntity: CategoryWithoutLevel::class)] + #[ORM\JoinColumn(name: 'ancestor', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected $ancestor; + + /** + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\CategoryWithoutLevel") + * @ORM\JoinColumn(name="descendant", referencedColumnName="id", nullable=false, onDelete="CASCADE") + */ + #[ORM\ManyToOne(targetEntity: CategoryWithoutLevel::class)] + #[ORM\JoinColumn(name: 'descendant', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected $descendant; } diff --git a/tests/Gedmo/Tree/Fixture/Closure/News.php b/tests/Gedmo/Tree/Fixture/Closure/News.php index 7c39d080b3..7bba467bb1 100644 --- a/tests/Gedmo/Tree/Fixture/Closure/News.php +++ b/tests/Gedmo/Tree/Fixture/Closure/News.php @@ -1,38 +1,54 @@ + +declare(strict_types=1); + +/* + * This file is part of the Doctrine Behavioral Extensions package. + * (c) Gediminas Morkevicius http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ -namespace Tree\Fixture\Closure; +namespace Gedmo\Tests\Tree\Fixture\Closure; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity + * + * @author Anatoly Marinescu */ +#[ORM\Entity] class News { /** + * @var int|null + * * @ORM\Id - * @ORM\Column(name="id", type="integer") * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private string $title; /** - * @ORM\OneToOne(targetEntity="Tree\Fixture\Closure\Category", cascade={"persist"}) + * @ORM\OneToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\Category", cascade={"persist"}) * @ORM\JoinColumn(name="category_id", referencedColumnName="id") */ - private $category; + #[ORM\OneToOne(targetEntity: Category::class, cascade: ['persist'])] + #[ORM\JoinColumn(name: 'category_id', referencedColumnName: 'id')] + private Category $category; - public function __construct($title, Category $category) + public function __construct(string $title, Category $category) { $this->title = $title; $this->category = $category; diff --git a/tests/Gedmo/Tree/Fixture/Closure/Person.php b/tests/Gedmo/Tree/Fixture/Closure/Person.php index fb8153e5f8..b2ab62fc89 100644 --- a/tests/Gedmo/Tree/Fixture/Closure/Person.php +++ b/tests/Gedmo/Tree/Fixture/Closure/Person.php @@ -1,93 +1,131 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Closure; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\ClosureTreeRepository; /** * @Gedmo\Tree(type="closure") - * @Gedmo\TreeClosure(class="Tree\Fixture\Closure\PersonClosure") + * @Gedmo\TreeClosure(class="Gedmo\Tests\Tree\Fixture\Closure\PersonClosure") + * * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\ClosureTreeRepository") * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="discriminator", type="string") * @ORM\DiscriminatorMap({ - "user" = "User" - }) + * "user": "User" + * }) */ +#[Gedmo\Tree(type: 'closure')] +#[Gedmo\TreeClosure(class: PersonClosure::class)] +#[ORM\Entity(repositoryClass: ClosureTreeRepository::class)] +#[ORM\InheritanceType('JOINED')] +#[ORM\DiscriminatorColumn(name: 'discriminator', type: Types::STRING)] +#[ORM\DiscriminatorMap(['user' => User::class])] abstract class Person { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="full_name", type="string", length=64) */ - private $fullName; + #[ORM\Column(name: 'full_name', type: Types::STRING, length: 64)] + private ?string $fullName = null; /** * @Gedmo\TreeParent + * * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * @ORM\ManyToOne(targetEntity="Person", inversedBy="children", cascade={"persist"}) */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children', cascade: ['persist'])] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?Person $parent = null; /** * @ORM\Column(name="level", type="integer") + * * @Gedmo\TreeLevel */ - private $level; + #[ORM\Column(name: 'level', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private ?int $level = null; + + private ?string $name = null; + + /** + * @var CategoryClosure[] + */ + private array $closures = []; - public function getId() + public function getId(): ?int { return $this->id; } - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } - public function getName() + public function getName(): ?string { return $this->name; } - public function setParent(Category $parent = null) + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function addClosure(CategoryClosure $closure) + public function addClosure(CategoryClosure $closure): void { $this->closures[] = $closure; } - public function setLevel($level) + public function setLevel(?int $level): void { $this->level = $level; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } - public function setFullName($fullName) + public function setFullName(?string $fullName): void { $this->fullName = $fullName; } - public function getFullName() + public function getFullName(): ?string { return $this->fullName; } diff --git a/tests/Gedmo/Tree/Fixture/Closure/PersonClosure.php b/tests/Gedmo/Tree/Fixture/Closure/PersonClosure.php index 100404c254..a2b5c2678b 100644 --- a/tests/Gedmo/Tree/Fixture/Closure/PersonClosure.php +++ b/tests/Gedmo/Tree/Fixture/Closure/PersonClosure.php @@ -1,13 +1,46 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Closure; -use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; /** * @ORM\Entity + * @ORM\Table( + * indexes={@ORM\Index(name="closure_person_depth_idx", columns={"depth"})}, + * uniqueConstraints={@ORM\UniqueConstraint(name="closure_person_unique_idx", columns={ + * "ancestor", "descendant" + * })} + * ) */ +#[ORM\Entity] +#[ORM\UniqueConstraint(name: 'closure_person_unique_idx', columns: ['ancestor', 'descendant'])] +#[ORM\Index(name: 'closure_person_depth_idx', columns: ['depth'])] class PersonClosure extends AbstractClosure { + /** + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\Person") + * @ORM\JoinColumn(name="ancestor", referencedColumnName="id", nullable=false, onDelete="CASCADE") + */ + #[ORM\ManyToOne(targetEntity: Person::class)] + #[ORM\JoinColumn(name: 'ancestor', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected $ancestor; + + /** + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Tree\Fixture\Closure\Person") + * @ORM\JoinColumn(name="descendant", referencedColumnName="id", nullable=false, onDelete="CASCADE") + */ + #[ORM\ManyToOne(targetEntity: Person::class)] + #[ORM\JoinColumn(name: 'descendant', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected $descendant; } diff --git a/tests/Gedmo/Tree/Fixture/Closure/User.php b/tests/Gedmo/Tree/Fixture/Closure/User.php index fcf7cfc622..e63532d449 100644 --- a/tests/Gedmo/Tree/Fixture/Closure/User.php +++ b/tests/Gedmo/Tree/Fixture/Closure/User.php @@ -1,25 +1,37 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Closure; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class User extends Person { /** * @ORM\Column(name="username", type="string", length=64) */ - private $username; + #[ORM\Column(name: 'username', type: Types::STRING, length: 64)] + private ?string $username = null; - public function setUsername($username) + public function setUsername(?string $username): void { $this->username = $username; } - public function getUsername() + public function getUsername(): ?string { return $this->username; } diff --git a/tests/Gedmo/Tree/Fixture/Comment.php b/tests/Gedmo/Tree/Fixture/Comment.php index 623b4c1354..8ab467b147 100644 --- a/tests/Gedmo/Tree/Fixture/Comment.php +++ b/tests/Gedmo/Tree/Fixture/Comment.php @@ -1,47 +1,65 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Comment { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="message", type="text") */ - private $message; + #[ORM\Column(name: 'message', type: Types::TEXT)] + private ?string $message = null; /** * @ORM\ManyToOne(targetEntity="Article", inversedBy="comments") */ - private $article; + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'comments')] + private ?Article $article = null; - public function setArticle($article) + public function setArticle(?Article $article): void { $this->article = $article; } - public function getId() + public function getId(): ?int { return $this->id; } - public function setMessage($message) + public function setMessage(?string $message): void { $this->message = $message; } - public function getMessage() + public function getMessage(): ?string { return $this->message; } diff --git a/tests/Gedmo/Tree/Fixture/Document/Article.php b/tests/Gedmo/Tree/Fixture/Document/Article.php index a8a42abf0b..2f47ab76c6 100644 --- a/tests/Gedmo/Tree/Fixture/Document/Article.php +++ b/tests/Gedmo/Tree/Fixture/Document/Article.php @@ -1,87 +1,125 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Document; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as Mongo; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ODM\MongoDB\Mapping\Annotations as MONGO; +use Gedmo\Tree\Document\MongoDB\Repository\MaterializedPathRepository; /** - * @MONGO\Document(repositoryClass="Gedmo\Tree\Document\MongoDB\Repository\MaterializedPathRepository") + * @Mongo\Document(repositoryClass="Gedmo\Tree\Document\MongoDB\Repository\MaterializedPathRepository") + * * @Gedmo\Tree(type="materializedPath", activateLocking=true) */ +#[Mongo\Document(repositoryClass: MaterializedPathRepository::class)] +#[Gedmo\Tree(type: 'materializedPath', activateLocking: true)] class Article { /** - * @MONGO\Id + * @var string|null + * + * @Mongo\Id */ + #[Mongo\Id] private $id; /** - * @MONGO\Field(type="string") + * @Mongo\Field(type="string") + * * @Gedmo\TreePathSource */ - private $title; + #[Mongo\Field(type: Type::STRING)] + #[Gedmo\TreePathSource] + private ?string $title = null; /** - * @MONGO\Field(type="string") + * @var string|null + * + * @Mongo\Field(type="string") + * * @Gedmo\TreePath(separator="|") */ + #[Mongo\Field(type: Type::STRING)] + #[Gedmo\TreePath(separator: '|')] private $path; /** * @Gedmo\TreeParent - * @MONGO\ReferenceOne(targetDocument="Article") + * + * @Mongo\ReferenceOne(targetDocument="Gedmo\Tests\Tree\Fixture\Document\Article") */ - private $parent; + #[Mongo\ReferenceOne(targetDocument: self::class)] + #[Gedmo\TreeParent] + private ?Article $parent = null; /** + * @var int|null + * * @Gedmo\TreeLevel - * @MONGO\Field(type="int") + * + * @Mongo\Field(type="int") */ + #[Mongo\Field(type: Type::INT)] + #[Gedmo\TreeLevel] private $level; /** + * @var \DateTime|null + * * @Gedmo\TreeLockTime - * @MONGO\Field(type="date") + * + * @Mongo\Field(type="date") */ + #[Mongo\Field(type: Type::DATE)] + #[Gedmo\TreeLockTime] private $lockTime; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(Article $parent = null) + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } - public function getPath() + public function getPath(): ?string { return $this->path; } - public function getLockTime() + public function getLockTime(): ?\DateTime { return $this->lockTime; } diff --git a/tests/Gedmo/Tree/Fixture/Document/Category.php b/tests/Gedmo/Tree/Fixture/Document/Category.php index f6e5102a0f..ba397fe392 100644 --- a/tests/Gedmo/Tree/Fixture/Document/Category.php +++ b/tests/Gedmo/Tree/Fixture/Document/Category.php @@ -1,76 +1,109 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Document; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as Mongo; +use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; -use Doctrine\ODM\MongoDB\Mapping\Annotations as MONGO; +use Gedmo\Tree\Document\MongoDB\Repository\MaterializedPathRepository; /** - * @MONGO\Document(repositoryClass="Gedmo\Tree\Document\MongoDB\Repository\MaterializedPathRepository") + * @Mongo\Document(repositoryClass="Gedmo\Tree\Document\MongoDB\Repository\MaterializedPathRepository") + * * @Gedmo\Tree(type="materializedPath") */ +#[Mongo\Document(repositoryClass: MaterializedPathRepository::class)] +#[Gedmo\Tree(type: 'materializedPath')] class Category { /** - * @MONGO\Id + * @var string|null + * + * @Mongo\Id */ + #[Mongo\Id] private $id; /** - * @MONGO\Field(type="string") + * @Mongo\Field(type="string") + * * @Gedmo\TreePathSource */ - private $title; + #[Mongo\Field(type: Type::STRING)] + #[Gedmo\TreePathSource] + private ?string $title = null; /** - * @MONGO\Field(type="string") + * @var string|null + * + * @Mongo\Field(type="string") + * * @Gedmo\TreePath(separator="|") */ + #[Mongo\Field(type: Type::STRING)] + #[Gedmo\TreePath(separator: '|')] private $path; /** * @Gedmo\TreeParent - * @MONGO\ReferenceOne(targetDocument="Category") + * + * @Mongo\ReferenceOne(targetDocument="Gedmo\Tests\Tree\Fixture\Document\Category") */ - private $parent; + #[Mongo\ReferenceOne(targetDocument: self::class)] + #[Gedmo\TreeParent] + private ?Category $parent = null; /** + * @var int|null + * * @Gedmo\TreeLevel - * @MONGO\Field(type="int") + * + * @Mongo\Field(type="int") */ + #[Mongo\Field(type: Type::INT)] + #[Gedmo\TreeLevel] private $level; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(Category $parent = null) + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } - public function getPath() + public function getPath(): ?string { return $this->path; } diff --git a/tests/Gedmo/Tree/Fixture/ForeignRootCategory.php b/tests/Gedmo/Tree/Fixture/ForeignRootCategory.php new file mode 100644 index 0000000000..55a545ff65 --- /dev/null +++ b/tests/Gedmo/Tree/Fixture/ForeignRootCategory.php @@ -0,0 +1,186 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; + +/** + * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") + * + * @Gedmo\Tree(type="nested") + */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[Gedmo\Tree(type: 'nested')] +class ForeignRootCategory +{ + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @var int|null + * + * @Gedmo\TreeLeft + * + * @ORM\Column(name="lft", type="integer") + */ + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private $lft; + + /** + * @var int|null + * + * @Gedmo\TreeRight + * + * @ORM\Column(name="rgt", type="integer") + */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] + private $rgt; + + /** + * @Gedmo\TreeParent + * + * @ORM\ManyToOne(targetEntity="ForeignRootCategory", inversedBy="children") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * }) + */ + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?ForeignRootCategory $parent = null; + + /** + * @var int|null + * + * @Gedmo\TreeRoot(identifierMethod="getRoot") + * + * @ORM\Column(type="integer") + */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeRoot(identifierMethod: 'getRoot')] + private $root; + + /** + * @var int|null + * + * @Gedmo\TreeLevel + * + * @ORM\Column(name="lvl", type="integer") + */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private $level; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="ForeignRootCategory", mappedBy="parent") + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private $children; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setParent(?self $parent = null): void + { + $this->parent = $parent; + } + + public function getParent(): ?self + { + return $this->parent; + } + + public function getRoot(): ?int + { + return $this->root; + } + + public function getLeft(): ?int + { + return $this->lft; + } + + public function getRight(): ?int + { + return $this->rgt; + } + + public function getLevel(): ?int + { + return $this->level; + } + + /** + * @return Collection + */ + public function getChildren(): Collection + { + return $this->children; + } + + /** + * @param Collection $children + */ + public function setChildren(Collection $children): void + { + $this->children = $children; + } + + /** + * @param mixed $root + */ + public function setRoot($root): void + { + $this->root = $root; + } +} diff --git a/tests/Gedmo/Tree/Fixture/Genealogy/Man.php b/tests/Gedmo/Tree/Fixture/Genealogy/Man.php index 9023267a16..8665aa9b59 100644 --- a/tests/Gedmo/Tree/Fixture/Genealogy/Man.php +++ b/tests/Gedmo/Tree/Fixture/Genealogy/Man.php @@ -1,11 +1,23 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Genealogy; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] class Man extends Person { } diff --git a/tests/Gedmo/Tree/Fixture/Genealogy/Person.php b/tests/Gedmo/Tree/Fixture/Genealogy/Person.php index c2a5f0284a..98d94adaa6 100644 --- a/tests/Gedmo/Tree/Fixture/Genealogy/Person.php +++ b/tests/Gedmo/Tree/Fixture/Genealogy/Person.php @@ -1,101 +1,137 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Genealogy; use Doctrine\Common\Collections\ArrayCollection; -use Gedmo\Mapping\Annotation as Gedmo; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") * @ORM\Table(name="genealogy") * @ORM\InheritanceType("SINGLE_TABLE") * @ORM\DiscriminatorColumn(name="discr", type="string") - * @ORM\DiscriminatorMap({"man" = "Man", "woman" = "Woman"}) + * @ORM\DiscriminatorMap({"man": "Man", "woman": "Woman"}) + * * @Gedmo\Tree(type="nested") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[ORM\Table(name: 'genealogy')] +#[ORM\InheritanceType('SINGLE_TABLE')] +#[ORM\DiscriminatorColumn(name: 'discr', type: Types::STRING)] +#[ORM\DiscriminatorMap(['man' => Man::class, 'woman' => Woman::class])] +#[Gedmo\Tree(type: 'nested')] abstract class Person { /** - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue - * @var int - */ - private $id; - - /** - * @Gedmo\TreeParent - * @ORM\ManyToOne(targetEntity="Person", inversedBy="children") - * @var Person - */ - private $parent; - - /** - * @ORM\OneToMany(targetEntity="Person", mappedBy="parent") - * @var Doctrine\Common\Collections\ArrayCollection - */ - protected $children; - - /** - * @Gedmo\TreeLeft - * @ORM\Column(name="lft", type="integer") - */ - private $lft; - - /** - * @Gedmo\TreeRight - * @ORM\Column(name="rgt", type="integer") - */ - private $rgt; - - /** - * @Gedmo\TreeLevel - * @ORM\Column(name="lvl", type="integer") - */ - private $lvl; - - /** - * @ORM\Column(name="name", type="string", length=255, nullable=false) - * @var string - */ - private $name; - - /** - * @param string $name - */ - public function __construct($name) - { - $this->name = $name; - $this->children = new ArrayCollection(); - } - - /** - * @param Person $parent - * @return Person - */ - public function setParent(Person $parent) - { - $this->parent = $parent; - - return $this; - } - - public function getName() + * @var Collection + * + * @ORM\OneToMany(targetEntity="Person", mappedBy="parent") + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + protected $children; + + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @Gedmo\TreeParent + * + * @ORM\ManyToOne(targetEntity="Person", inversedBy="children") + */ + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[Gedmo\TreeParent] + private ?Person $parent = null; + + /** + * @var int|null + * + * @Gedmo\TreeLeft + * + * @ORM\Column(name="lft", type="integer") + */ + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private $lft; + + /** + * @var int|null + * + * @Gedmo\TreeRight + * + * @ORM\Column(name="rgt", type="integer") + */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] + private $rgt; + + /** + * @var int|null + * + * @Gedmo\TreeLevel + * + * @ORM\Column(name="lvl", type="integer") + */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private $lvl; + + /** + * @ORM\Column(name="name", type="string", length=191, nullable=false) + */ + #[ORM\Column(name: 'name', type: Types::STRING, length: 191, nullable: false)] + private string $name; + + public function __construct(string $name) + { + $this->name = $name; + $this->children = new ArrayCollection(); + } + + public function setParent(self $parent): self + { + $this->parent = $parent; + + return $this; + } + + public function getName(): string { return $this->name; } - public function getLeft() + public function getLeft(): ?int { return $this->lft; } - public function getRight() + public function getRight(): ?int { return $this->rgt; } - public function getLevel() + public function getLevel(): ?int { return $this->lvl; } diff --git a/tests/Gedmo/Tree/Fixture/Genealogy/Woman.php b/tests/Gedmo/Tree/Fixture/Genealogy/Woman.php index 937173bfa2..16a6b7a87d 100644 --- a/tests/Gedmo/Tree/Fixture/Genealogy/Woman.php +++ b/tests/Gedmo/Tree/Fixture/Genealogy/Woman.php @@ -1,11 +1,23 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Genealogy; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] class Woman extends Person { } diff --git a/tests/Gedmo/Tree/Fixture/Issue2408/Category.php b/tests/Gedmo/Tree/Fixture/Issue2408/Category.php new file mode 100644 index 0000000000..8cc5480463 --- /dev/null +++ b/tests/Gedmo/Tree/Fixture/Issue2408/Category.php @@ -0,0 +1,151 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Issue2408; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; + +/** + * @Gedmo\Tree(type="nested") + * + * @ORM\Table(name="categories") + * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") + */ +#[Gedmo\Tree(type: 'nested')] +#[ORM\Table(name: 'categories')] +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +class Category +{ + /** + * @var int|null + * + * @ORM\Column(name="id", type="integer") + * @ORM\Id + * @ORM\GeneratedValue + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @var int|null + * + * @Gedmo\TreeLeft + * + * @ORM\Column(name="lft", type="integer") + */ + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private $lft; + + /** + * @var int|null + * + * @Gedmo\TreeRight + * + * @ORM\Column(name="rgt", type="integer") + */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] + private $rgt; + + /** + * @var int|null + * + * @Gedmo\TreeLevel + * + * @ORM\Column(name="lvl", type="integer") + */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private $lvl; + + /** + * @var self|null + * + * @Gedmo\TreeRoot + * + * @ORM\ManyToOne(targetEntity="Category") + * @ORM\JoinColumn(name="tree_root", referencedColumnName="id", onDelete="CASCADE") + */ + #[Gedmo\TreeRoot] + #[ORM\ManyToOne(targetEntity: self::class)] + #[ORM\JoinColumn(name: 'tree_root', referencedColumnName: 'id', onDelete: 'CASCADE')] + private $root; + + /** + * @Gedmo\TreeParent + * + * @ORM\ManyToOne(targetEntity="Category", inversedBy="children") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + */ + #[Gedmo\TreeParent] + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + private ?Category $parent = null; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="Category", mappedBy="parent") + * @ORM\OrderBy({"lft": "ASC"}) + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + #[ORM\OrderBy(['lft' => 'ASC'])] + private Collection $children; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function getRoot(): ?self + { + return $this->root; + } + + public function setParent(?self $parent = null): void + { + $this->parent = $parent; + } + + public function getParent(): ?self + { + return $this->parent; + } +} diff --git a/tests/Gedmo/Tree/Fixture/Issue2517/Category.php b/tests/Gedmo/Tree/Fixture/Issue2517/Category.php new file mode 100644 index 0000000000..c00494078b --- /dev/null +++ b/tests/Gedmo/Tree/Fixture/Issue2517/Category.php @@ -0,0 +1,151 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Issue2517; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; + +/** + * @Gedmo\Tree(type="nested") + * + * @ORM\Table(name="categories") + * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") + */ +#[Gedmo\Tree(type: 'nested')] +#[ORM\Table(name: 'categories')] +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +class Category +{ + /** + * @var int|null + * + * @ORM\Column(name="id", type="integer") + * @ORM\Id + * @ORM\GeneratedValue + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @var int|null + * + * @Gedmo\TreeLeft + * + * @ORM\Column(name="lft", type="integer") + */ + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private $lft; + + /** + * @var int|null + * + * @Gedmo\TreeRight + * + * @ORM\Column(name="rgt", type="integer") + */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] + private $rgt; + + /** + * @var int|null + * + * @Gedmo\TreeLevel + * + * @ORM\Column(name="lvl", type="integer") + */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private $lvl; + + /** + * @var self|null + * + * @Gedmo\TreeRoot + * + * @ORM\ManyToOne(targetEntity="Category") + * @ORM\JoinColumn(name="tree_root", referencedColumnName="id", onDelete="CASCADE") + */ + #[Gedmo\TreeRoot] + #[ORM\ManyToOne(targetEntity: self::class)] + #[ORM\JoinColumn(name: 'tree_root', referencedColumnName: 'id', onDelete: 'CASCADE')] + private $root; + + /** + * @Gedmo\TreeParent + * + * @ORM\ManyToOne(targetEntity="Category", inversedBy="children") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + */ + #[Gedmo\TreeParent] + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + private ?Category $parent = null; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="Category", mappedBy="parent") + * @ORM\OrderBy({"lft" = "ASC"}) + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + #[ORM\OrderBy(['lft' => 'ASC'])] + private Collection $children; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function getRoot(): ?self + { + return $this->root; + } + + public function setParent(?self $parent): void + { + $this->parent = $parent; + } + + public function getParent(): ?self + { + return $this->parent; + } +} diff --git a/tests/Gedmo/Tree/Fixture/Issue2616/Category.php b/tests/Gedmo/Tree/Fixture/Issue2616/Category.php new file mode 100644 index 0000000000..ab90a9906c --- /dev/null +++ b/tests/Gedmo/Tree/Fixture/Issue2616/Category.php @@ -0,0 +1,176 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Issue2616; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @Gedmo\Tree(type="materializedPath") + * + * @ORM\Table(name="category") + * @ORM\Entity + */ +#[ORM\Table(name: 'category')] +#[ORM\Entity] +#[Gedmo\Tree(type: 'materializedPath')] +class Category +{ + /** + * @ORM\ManyToOne(targetEntity="\Gedmo\Tests\Tree\Fixture\Issue2616\Category", inversedBy="children") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="category_id", onDelete="cascade") + * + * @Gedmo\TreeParent + * + * @var Category|null + */ + #[Gedmo\TreeParent] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'category_id', onDelete: 'cascade')] + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + protected $parent; + + /** + * @ORM\OneToMany(targetEntity="\Gedmo\Tests\Tree\Fixture\Issue2616\Category", mappedBy="parent", fetch="EXTRA_LAZY") + * + * @var Category[]|null + */ + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, fetch: 'EXTRA_LAZY')] + protected $children; + + /** + * @ORM\OneToOne(targetEntity="\Gedmo\Tests\Tree\Fixture\Issue2616\Page", mappedBy="category", cascade={"remove"}) + * + * @var Page|null + */ + #[ORM\OneToOne(targetEntity: Page::class, mappedBy: 'category', cascade: ['remove'])] + protected $page; + /** + * @var int|null + * + * @ORM\Column(name="category_id", type="integer") + * @ORM\Id + * @ORM\GeneratedValue + * + * @Gedmo\TreePathSource + */ + #[Gedmo\TreePathSource] + #[ORM\Column(name: 'category_id', type: Types::INTEGER)] + #[ORM\GeneratedValue] + #[ORM\Id] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @Gedmo\TreeLevel + * + * @ORM\Column(name="level", type="integer", nullable=true) + * + * @var int|null + */ + #[Gedmo\TreeLevel] + #[ORM\Column(name: 'level', type: Types::INTEGER, nullable: true)] + private $level; + + /** + * @Gedmo\TreePath(separator="/", endsWithSeparator=false) + * + * @ORM\Column(name="path", type="string", nullable=true) + * + * @var string|null + */ + #[ORM\Column(name: 'path', type: Types::STRING, nullable: true)] + #[Gedmo\TreePath(separator: '/', endsWithSeparator: false)] + private $path; + + public function getId(): ?int + { + return $this->id; + } + + /** + * @return Category|null + */ + public function getParent() + { + return $this->parent; + } + + /** + * @param Category|null $parent + */ + public function setParent($parent): void + { + $this->parent = $parent; + } + + /** + * @return Page|null + */ + public function getPage() + { + return $this->page; + } + + public function setPage(Page $page): void + { + $this->page = $page; + $page->setCategory($this); + } + + /** + * @return int + */ + public function getLevel() + { + return $this->level; + } + + /** + * @param int $level + */ + public function setLevel($level): void + { + $this->level = $level; + } + + /** + * @return string|null + */ + public function getPath() + { + return $this->path; + } + + /** + * @param string $path + * + * @return void + */ + public function setPath($path) + { + $this->path = $path; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } +} diff --git a/tests/Gedmo/Tree/Fixture/Issue2616/Page.php b/tests/Gedmo/Tree/Fixture/Issue2616/Page.php new file mode 100644 index 0000000000..6f15d52d55 --- /dev/null +++ b/tests/Gedmo/Tree/Fixture/Issue2616/Page.php @@ -0,0 +1,73 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Issue2616; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + * @ORM\Table(name="page") + */ +#[ORM\Entity, ORM\Table(name: 'page')] +class Page +{ + /** + * @var Category|null + * + * @ORM\OneToOne(targetEntity="Category", inversedBy="page") + * @ORM\JoinColumn(name="entity_id", referencedColumnName="category_id", nullable=false) + */ + #[ORM\JoinColumn(name: 'entity_id', referencedColumnName: 'category_id', nullable: false)] + #[ORM\OneToOne(targetEntity: Category::class, inversedBy: 'page')] + protected $category; + /** + * @var int|null + * + * @ORM\Column(name="page_id", type="integer") + * @ORM\Id + * @ORM\GeneratedValue + */ + #[ORM\Column(name: 'page_id', type: Types::INTEGER)] + #[ORM\GeneratedValue] + #[ORM\Id] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getCategory(): ?Category + { + return $this->category; + } + + public function setCategory(?Category $category): void + { + $this->category = $category; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } +} diff --git a/tests/Gedmo/Tree/Fixture/MPCategory.php b/tests/Gedmo/Tree/Fixture/MPCategory.php index e83345ba66..1d5ee194c2 100644 --- a/tests/Gedmo/Tree/Fixture/MPCategory.php +++ b/tests/Gedmo/Tree/Fixture/MPCategory.php @@ -1,97 +1,161 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\MaterializedPathRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\MaterializedPathRepository") + * * @Gedmo\Tree(type="materializedPath") */ +#[ORM\Entity(repositoryClass: MaterializedPathRepository::class)] +#[Gedmo\Tree(type: 'materializedPath')] class MPCategory { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\TreePath + * * @ORM\Column(name="path", type="string", length=3000, nullable=true) */ - private $path; + #[ORM\Column(name: 'path', type: Types::STRING, length: 3000, nullable: true)] + #[Gedmo\TreePath] + private ?string $path = null; /** * @Gedmo\TreePathSource + * * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + #[Gedmo\TreePathSource] + private ?string $title = null; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="MPCategory", inversedBy="children") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * }) */ - private $parentId; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?MPCategory $parentId = null; /** + * @var int|null + * * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer", nullable=true) */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLevel] private $level; /** + * @var string|null + * + * @Gedmo\TreeRoot + * + * @ORM\Column(name="tree_root_value", type="string", nullable=true) + */ + #[ORM\Column(name: 'tree_root_value', type: Types::STRING, nullable: true)] + #[Gedmo\TreeRoot] + private $treeRootValue; + + /** + * @var Collection + * * @ORM\OneToMany(targetEntity="MPCategory", mappedBy="parent") */ - private $children; + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="category") */ - private $comments; + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')] + private Collection $comments; + + public function __construct() + { + $this->children = new ArrayCollection(); + $this->comments = new ArrayCollection(); + } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(MPCategory $parent = null) + public function setParent(?self $parent = null): void { $this->parentId = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parentId; } - public function setPath($path) + public function setPath(?string $path): void { $this->path = $path; } - public function getPath() + public function getPath(): ?string { return $this->path; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } + + public function getTreeRootValue(): ?string + { + return $this->treeRootValue; + } } diff --git a/tests/Gedmo/Tree/Fixture/MPCategoryUuid.php b/tests/Gedmo/Tree/Fixture/MPCategoryUuid.php new file mode 100644 index 0000000000..b0b517da78 --- /dev/null +++ b/tests/Gedmo/Tree/Fixture/MPCategoryUuid.php @@ -0,0 +1,159 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\MaterializedPathRepository; +use Symfony\Component\Uid\UuidV4; + +/** + * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\MaterializedPathRepository") + * + * @Gedmo\Tree(type="materializedPath") + */ +#[ORM\Entity(repositoryClass: MaterializedPathRepository::class)] +#[Gedmo\Tree(type: 'materializedPath')] +class MPCategoryUuid +{ + /** + * @Gedmo\TreePathSource + * + * @ORM\Id + * @ORM\Column(type="uuid") + */ + #[ORM\Id] + #[ORM\Column(type: 'uuid')] + #[Gedmo\TreePathSource] + private UuidV4 $id; + + /** + * @Gedmo\TreePath + * + * @ORM\Column(name="path", type="string", length=3000, nullable=true) + */ + #[ORM\Column(name: 'path', type: Types::STRING, length: 3000, nullable: true)] + #[Gedmo\TreePath] + private ?string $path = null; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @Gedmo\TreeParent + * + * @ORM\ManyToOne(targetEntity="MPCategoryUuid", inversedBy="children") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * }) + */ + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?MPCategoryUuid $parentId = null; + + /** + * @var int|null + * + * @Gedmo\TreeLevel + * + * @ORM\Column(name="lvl", type="integer", nullable=true) + */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLevel] + private $level; + + /** + * @var string|null + * + * @Gedmo\TreeRoot + * + * @ORM\Column(name="tree_root_value", type="string", nullable=true) + */ + #[ORM\Column(name: 'tree_root_value', type: Types::STRING, nullable: true)] + #[Gedmo\TreeRoot] + private $treeRootValue; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="MPCategoryUuid", mappedBy="parent") + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="Article", mappedBy="category") + */ + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')] + private Collection $comments; + + public function __construct() + { + $this->id = new UuidV4(); + $this->children = new ArrayCollection(); + $this->comments = new ArrayCollection(); + } + + public function getId(): ?UuidV4 + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function setParent(?self $parent = null): void + { + $this->parentId = $parent; + } + + public function getParent(): ?self + { + return $this->parentId; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function setPath(?string $path): void + { + $this->path = $path; + } + + public function getLevel(): ?int + { + return $this->level; + } + + public function getTreeRootValue(): ?string + { + return $this->treeRootValue; + } +} diff --git a/tests/Gedmo/Tree/Fixture/MPCategoryWithRootAssociation.php b/tests/Gedmo/Tree/Fixture/MPCategoryWithRootAssociation.php new file mode 100644 index 0000000000..7afe346ad1 --- /dev/null +++ b/tests/Gedmo/Tree/Fixture/MPCategoryWithRootAssociation.php @@ -0,0 +1,165 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\MaterializedPathRepository; + +/** + * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\MaterializedPathRepository") + * + * @Gedmo\Tree(type="materializedPath") + */ +#[ORM\Entity(repositoryClass: MaterializedPathRepository::class)] +#[Gedmo\Tree(type: 'materializedPath')] +class MPCategoryWithRootAssociation +{ + /** + * @var int|null + * + * @Gedmo\TreePathSource + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreePathSource] + private $id; + + /** + * @Gedmo\TreePath + * + * @ORM\Column(name="path", type="string", length=3000, nullable=true) + */ + #[ORM\Column(name: 'path', type: Types::STRING, length: 3000, nullable: true)] + #[Gedmo\TreePath] + private ?string $path = null; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @Gedmo\TreeParent + * + * @ORM\ManyToOne(targetEntity="MPCategoryWithRootAssociation", inversedBy="children") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * }) + */ + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?MPCategoryWithRootAssociation $parentId = null; + + /** + * @var int|null + * + * @Gedmo\TreeLevel + * + * @ORM\Column(name="lvl", type="integer", nullable=true) + */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLevel] + private $level; + + /** + * @var self|null + * + * @Gedmo\TreeRoot + * + * @ORM\ManyToOne(targetEntity="MPCategoryWithRootAssociation") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="tree_root_entity", referencedColumnName="id", onDelete="CASCADE") + * }) + */ + #[ORM\ManyToOne(targetEntity: self::class)] + #[ORM\JoinColumn(name: 'tree_root_entity', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeRoot] + private $treeRootEntity; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="MPCategory", mappedBy="parent") + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="Article", mappedBy="category") + */ + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')] + private Collection $comments; + + public function __construct() + { + $this->children = new ArrayCollection(); + $this->comments = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setParent(?self $parent = null): void + { + $this->parentId = $parent; + } + + public function getParent(): ?self + { + return $this->parentId; + } + + public function setPath(?string $path): void + { + $this->path = $path; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getLevel(): ?int + { + return $this->level; + } + + public function getTreeRootEntity(): ?self + { + return $this->treeRootEntity; + } +} diff --git a/tests/Gedmo/Tree/Fixture/MPCategoryWithTrimmedSeparator.php b/tests/Gedmo/Tree/Fixture/MPCategoryWithTrimmedSeparator.php index 34f47b2048..28d3ca8fd9 100644 --- a/tests/Gedmo/Tree/Fixture/MPCategoryWithTrimmedSeparator.php +++ b/tests/Gedmo/Tree/Fixture/MPCategoryWithTrimmedSeparator.php @@ -1,91 +1,135 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\MaterializedPathRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\MaterializedPathRepository") + * * @Gedmo\Tree(type="materializedPath") */ +#[ORM\Entity(repositoryClass: MaterializedPathRepository::class)] +#[Gedmo\Tree(type: 'materializedPath')] class MPCategoryWithTrimmedSeparator { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\TreePath(appendId=false, startsWithSeparator=false, endsWithSeparator=false) + * * @ORM\Column(name="path", type="string", length=3000, nullable=true) */ - private $path; + #[ORM\Column(name: 'path', type: Types::STRING, length: 3000, nullable: true)] + #[Gedmo\TreePath(appendId: false, startsWithSeparator: false, endsWithSeparator: false)] + private ?string $path = null; /** * @Gedmo\TreePathSource + * * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + #[Gedmo\TreePathSource] + private ?string $title = null; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="MPCategoryWithTrimmedSeparator", inversedBy="children") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * }) */ - private $parentId; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?MPCategoryWithTrimmedSeparator $parentId = null; /** + * @var int|null + * * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer", nullable=true) */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLevel] private $level; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="MPCategoryWithTrimmedSeparator", mappedBy="parent") */ - private $children; + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; + + public function __construct() + { + $this->children = new ArrayCollection(); + } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(MPCategoryWithTrimmedSeparator $parent = null) + public function setParent(?self $parent = null): void { $this->parentId = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parentId; } - public function setPath($path) + public function setPath(?string $path): void { $this->path = $path; } - public function getPath() + public function getPath(): ?string { return $this->path; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } diff --git a/tests/Gedmo/Tree/Fixture/MPFeaturesCategory.php b/tests/Gedmo/Tree/Fixture/MPFeaturesCategory.php index d0ce63a5b6..c88e428e49 100644 --- a/tests/Gedmo/Tree/Fixture/MPFeaturesCategory.php +++ b/tests/Gedmo/Tree/Fixture/MPFeaturesCategory.php @@ -1,107 +1,176 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\MaterializedPathRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\MaterializedPathRepository") + * * @Gedmo\Tree(type="materializedPath") */ +#[ORM\Entity(repositoryClass: MaterializedPathRepository::class)] +#[Gedmo\Tree(type: 'materializedPath')] class MPFeaturesCategory { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @Gedmo\TreePath(appendId=false, startsWithSeparator=true, endsWithSeparator=false) + * * @ORM\Column(name="path", type="string", length=3000, nullable=true) */ - private $path; + #[ORM\Column(name: 'path', type: Types::STRING, length: 3000, nullable: true)] + #[Gedmo\TreePath(appendId: false, startsWithSeparator: true, endsWithSeparator: false)] + private ?string $path = null; /** + * @var string|null + * * @Gedmo\TreePathHash + * * @ORM\Column(name="pathhash", type="string", length=32, nullable=true) */ + #[ORM\Column(name: 'pathhash', type: Types::STRING, length: 32, nullable: true)] + #[Gedmo\TreePathHash] private $pathHash; /** * @Gedmo\TreePathSource + * * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + #[Gedmo\TreePathSource] + private ?string $title = null; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="MPFeaturesCategory", inversedBy="children") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * }) */ - private $parentId; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?MPFeaturesCategory $parentId = null; /** + * @var int|null + * * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer", nullable=true) */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLevel] private $level; /** + * @var string|null + * + * @Gedmo\TreeRoot + * + * @ORM\Column(name="tree_root_value", type="string", nullable=true) + */ + #[ORM\Column(name: 'tree_root_value', type: Types::STRING, nullable: true)] + #[Gedmo\TreeRoot] + private $treeRootValue; + + /** + * @var Collection + * * @ORM\OneToMany(targetEntity="MPFeaturesCategory", mappedBy="parent") */ - private $children; + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="Article", mappedBy="category") */ - private $comments; + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')] + private Collection $comments; + + public function __construct() + { + $this->children = new ArrayCollection(); + $this->comments = new ArrayCollection(); + } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(MPFeaturesCategory $parent = null) + public function setParent(?self $parent = null): void { $this->parentId = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parentId; } - public function setPath($path) + public function setPath(?string $path): void { $this->path = $path; } - public function getPath() + public function getPath(): ?string { return $this->path; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } - public function getPathHash() + public function getTreeRootValue(): ?string + { + return $this->treeRootValue; + } + + public function getPathHash(): ?string { return $this->pathHash; } diff --git a/tests/Gedmo/Tree/Fixture/Mock/MaterializedPathMock.php b/tests/Gedmo/Tree/Fixture/Mock/MaterializedPathMock.php index e850f66cfc..16d4d5f079 100644 --- a/tests/Gedmo/Tree/Fixture/Mock/MaterializedPathMock.php +++ b/tests/Gedmo/Tree/Fixture/Mock/MaterializedPathMock.php @@ -1,25 +1,34 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Mock; + +use Doctrine\Persistence\ObjectManager; +use Gedmo\Mapping\Event\AdapterInterface; +use Gedmo\Tree\Strategy\ODM\MongoDB\MaterializedPath; + /** * Mock to test concurrency in MaterializedPath strategy * * @author Gustavo Adrian * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ - -namespace Tree\Fixture\Mock; - -use Gedmo\Tree\Strategy\ODM\MongoDB\MaterializedPath; -use Doctrine\Common\Persistence\ObjectManager; -use Gedmo\Mapping\Event\AdapterInterface; - -class MaterializedPathMock extends MaterializedPath +final class MaterializedPathMock extends MaterializedPath { + /** + * @var bool + */ public $releaseLocks = false; - protected function releaseTreeLocks(ObjectManager $om, AdapterInterface $ea) + protected function releaseTreeLocks(ObjectManager $om, AdapterInterface $ea): void { if ($this->releaseLocks) { parent::releaseTreeLocks($om, $ea); diff --git a/tests/Gedmo/Tree/Fixture/Mock/TreeListenerMock.php b/tests/Gedmo/Tree/Fixture/Mock/TreeListenerMock.php index 7a291412f5..a47c5051dc 100644 --- a/tests/Gedmo/Tree/Fixture/Mock/TreeListenerMock.php +++ b/tests/Gedmo/Tree/Fixture/Mock/TreeListenerMock.php @@ -1,46 +1,60 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Mock; + +use Doctrine\Persistence\ObjectManager; +use Gedmo\Tree\Strategy; +use Gedmo\Tree\TreeListener; + /** * Mock to test concurrency in MaterializedPath strategy * * @author Gustavo Adrian * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ - -namespace Tree\Fixture\Mock; - -use Gedmo\Tree\TreeListener; -use Doctrine\Common\Persistence\ObjectManager; - -class TreeListenerMock extends TreeListener +final class TreeListenerMock extends TreeListener { + /** + * @var bool + */ public $releaseLocks = false; - protected $strategy = null; + + /** + * @var MaterializedPathMock + */ + protected $strategy; public function getStrategy(ObjectManager $om, $class) { if (null === $this->strategy) { $this->strategy = new MaterializedPathMock($this); - $this->strategy->releaseLock = $this->releaseLocks; + $this->strategy->releaseLocks = $this->releaseLocks; } return $this->strategy; } - protected function getStrategiesUsedForObjects(array $classes) + public function setReleaseLocks(bool $bool): void + { + $this->strategy->releaseLocks = $bool; + } + + protected function getStrategiesUsedForObjects(array $classes): array { if (null === $this->strategy) { $this->strategy = new MaterializedPathMock($this); - $this->strategy->releaseLock = $this->releaseLocks; + $this->strategy->releaseLocks = $this->releaseLocks; } - return array('materializedPath' => $this->strategy); - } - - public function setReleaseLocks($bool) - { - $this->strategy->releaseLocks = $bool; + return ['materializedPath' => $this->strategy]; } } diff --git a/tests/Gedmo/Tree/Fixture/Node.php b/tests/Gedmo/Tree/Fixture/Node.php index 024ad70bd2..a003c02627 100644 --- a/tests/Gedmo/Tree/Fixture/Node.php +++ b/tests/Gedmo/Tree/Fixture/Node.php @@ -1,39 +1,60 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] class Node extends BaseNode { /** * @Gedmo\Translatable + * * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + #[Gedmo\Translatable] + private ?string $title = null; /** + * @var string|null + * * @Gedmo\Translatable * @Gedmo\Slug(fields={"title"}) + * * @ORM\Column(name="slug", type="string", length=128) */ + #[ORM\Column(name: 'slug', type: Types::STRING, length: 128)] + #[Gedmo\Translatable] + #[Gedmo\Slug(fields: ['title'])] private $slug; - public function getSlug() + public function getSlug(): ?string { return $this->slug; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Tree/Fixture/Repository/BehavioralCategoryRepository.php b/tests/Gedmo/Tree/Fixture/Repository/BehavioralCategoryRepository.php index 4069818d43..a3ce3ff266 100644 --- a/tests/Gedmo/Tree/Fixture/Repository/BehavioralCategoryRepository.php +++ b/tests/Gedmo/Tree/Fixture/Repository/BehavioralCategoryRepository.php @@ -1,9 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Repository; + +use Gedmo\Tests\Tree\Fixture\BehavioralCategory; use Gedmo\Tree\Entity\Repository\NestedTreeRepository; -class BehavioralCategoryRepository extends NestedTreeRepository +/** + * @template-extends NestedTreeRepository + */ +final class BehavioralCategoryRepository extends NestedTreeRepository { } diff --git a/tests/Gedmo/Tree/Fixture/Role.php b/tests/Gedmo/Tree/Fixture/Role.php index 5988571a0b..ebad728ecf 100644 --- a/tests/Gedmo/Tree/Fixture/Role.php +++ b/tests/Gedmo/Tree/Fixture/Role.php @@ -1,123 +1,159 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; use Doctrine\Common\Collections\ArrayCollection; -use Gedmo\Mapping\Annotation as Gedmo; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") * @ORM\Table(name="role") * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="discr", type="string") - * @ORM\DiscriminatorMap({"user" = "User", "usergroup" = "UserGroup", "userldap" = "UserLDAP"}) + * @ORM\DiscriminatorMap({"user": "User", "usergroup": "UserGroup", "userldap": "UserLDAP"}) + * * @Gedmo\Tree(type="nested") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[ORM\Table(name: 'role')] +#[ORM\InheritanceType('JOINED')] +#[ORM\DiscriminatorColumn(name: 'discr', type: Types::STRING)] +#[ORM\DiscriminatorMap(['user' => User::class, 'usergroup' => UserGroup::class, 'userldap' => UserLDAP::class])] +#[Gedmo\Tree(type: 'nested')] abstract class Role { /** - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue - * @var int - */ - private $id; - - /** - * @Gedmo\TreeParent - * @ORM\ManyToOne(targetEntity="UserGroup", inversedBy="children") - * @var UserGroup - */ - private $parent; - - /** - * @ORM\OneToMany(targetEntity="Role", mappedBy="parent") - * @var Doctrine\Common\Collections\ArrayCollection - */ - protected $children; - - /** - * @Gedmo\TreeLeft - * @ORM\Column(name="lft", type="integer") - */ - private $lft; - - /** - * @Gedmo\TreeRight - * @ORM\Column(name="rgt", type="integer") - */ - private $rgt; - - /** - * @Gedmo\TreeLevel - * @ORM\Column(name="lvl", type="integer") - */ - private $lvl; - - /** - * @ORM\Column(name="role", type="string", length=255, nullable=false) - * @var string - */ - private $role; + * @var Collection + * + * @ORM\OneToMany(targetEntity="Role", mappedBy="parent") + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + protected $children; + + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @Gedmo\TreeParent + * + * @ORM\ManyToOne(targetEntity="UserGroup", inversedBy="children") + */ + #[ORM\ManyToOne(targetEntity: UserGroup::class, inversedBy: 'children')] + #[Gedmo\TreeParent] + private ?UserGroup $parent = null; + + /** + * @var int|null + * + * @Gedmo\TreeLeft + * + * @ORM\Column(name="lft", type="integer") + */ + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private $lft; + + /** + * @var int|null + * + * @Gedmo\TreeRight + * + * @ORM\Column(name="rgt", type="integer") + */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] + private $rgt; + + /** + * @var int|null + * + * @Gedmo\TreeLevel + * + * @ORM\Column(name="lvl", type="integer") + */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private $lvl; + + /** + * @ORM\Column(name="role", type="string", length=191, nullable=false) + */ + #[ORM\Column(name: 'role', type: Types::STRING, length: 191, nullable: false)] + private ?string $role = null; public function __construct() { - $this->children = new ArrayCollection(); + $this->children = new ArrayCollection(); } - /** - * @return UserGroup - */ - public function getParent() - { - return $this->parent; - } - - /** - * @param UserGroup $parent - * @return Role - */ - public function setParent(UserGroup $parent) - { - $this->parent = $parent; - - return $this; - } - - public function getRoleId() + public function __toString(): string { - return $this->role; + return (string) $this->getRoleId(); } - protected function setRoleId($roleId) + public function getParent(): UserGroup { - $this->role = (string) $roleId; + return $this->parent; + } + + public function setParent(UserGroup $parent): self + { + $this->parent = $parent; return $this; } - public function __toString() + public function getRoleId(): ?string { - return $this->getRoleId(); + return $this->role; } - public function getId() + public function getId(): ?int { return $this->id; } - public function getLeft() + public function getLeft(): ?int { return $this->lft; } - public function getRight() + public function getRight(): ?int { return $this->rgt; } - public function getLevel() + public function getLevel(): ?int { return $this->lvl; } + + protected function setRoleId(?string $roleId): self + { + $this->role = (string) $roleId; + + return $this; + } } diff --git a/tests/Gedmo/Tree/Fixture/RootAssociationCategory.php b/tests/Gedmo/Tree/Fixture/RootAssociationCategory.php new file mode 100644 index 0000000000..bf1e9342d0 --- /dev/null +++ b/tests/Gedmo/Tree/Fixture/RootAssociationCategory.php @@ -0,0 +1,165 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; + +/** + * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") + * + * @Gedmo\Tree(type="nested") + */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[Gedmo\Tree(type: 'nested')] +class RootAssociationCategory +{ + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="RootAssociationCategory", mappedBy="parent") + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + protected $children; + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @var int|null + * + * @Gedmo\TreeLeft + * + * @ORM\Column(name="lft", type="integer") + */ + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] + private $lft; + + /** + * @var int|null + * + * @Gedmo\TreeRight + * + * @ORM\Column(name="rgt", type="integer") + */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] + private $rgt; + + /** + * @Gedmo\TreeParent + * + * @ORM\ManyToOne(targetEntity="RootAssociationCategory", inversedBy="children") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * }) + */ + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?RootAssociationCategory $parent = null; + + /** + * @var self|null + * + * @Gedmo\TreeRoot + * + * @ORM\ManyToOne(targetEntity="RootAssociationCategory") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="tree_root", referencedColumnName="id", onDelete="CASCADE") + * }) + */ + #[ORM\ManyToOne(targetEntity: self::class)] + #[ORM\JoinColumn(name: 'tree_root', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeRoot] + private $root; + + /** + * @var int|null + * + * @Gedmo\TreeLevel + * + * @ORM\Column(name="lvl", type="integer") + */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel] + private $level; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setParent(?self $parent = null): void + { + $this->parent = $parent; + } + + public function getParent(): ?self + { + return $this->parent; + } + + public function getRoot(): ?self + { + return $this->root; + } + + public function getLeft(): ?int + { + return $this->lft; + } + + public function getRight(): ?int + { + return $this->rgt; + } + + public function getLevel(): ?int + { + return $this->level; + } +} diff --git a/tests/Gedmo/Tree/Fixture/RootCategory.php b/tests/Gedmo/Tree/Fixture/RootCategory.php index 5b73c2a9c7..53bee18358 100644 --- a/tests/Gedmo/Tree/Fixture/RootCategory.php +++ b/tests/Gedmo/Tree/Fixture/RootCategory.php @@ -1,108 +1,190 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; +use Gedmo\Tree\Node; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") + * * @Gedmo\Tree(type="nested") */ -class RootCategory +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[Gedmo\Tree(type: 'nested')] +class RootCategory implements Node { /** - * @ORM\Column(name="id", type="integer") + * @var Collection + * + * @ORM\OneToMany(targetEntity="RootCategory", mappedBy="parent") + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + protected $children; + /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", length=64) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; /** + * @var int|null + * * @Gedmo\TreeLeft + * * @ORM\Column(name="lft", type="integer") */ + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + #[Gedmo\TreeLeft] private $lft; /** + * @var int|null + * * @Gedmo\TreeRight + * * @ORM\Column(name="rgt", type="integer") */ + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + #[Gedmo\TreeRight] private $rgt; /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="RootCategory", inversedBy="children") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * }) */ - private $parent; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?RootCategory $parent = null; /** + * @var int|null + * * @Gedmo\TreeRoot + * * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] + #[Gedmo\TreeRoot] private $root; /** - * @Gedmo\TreeLevel + * @var int|null + * + * @Gedmo\TreeLevel(base=1) + * * @ORM\Column(name="lvl", type="integer") */ - private $level; + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + #[Gedmo\TreeLevel(base: 1)] + private $level; - /** - * @ORM\OneToMany(targetEntity="RootCategory", mappedBy="parent") - */ - private $children; + private ?Node $sibling = null; - public function getId() + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setParent(RootCategory $parent = null) + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function getRoot() + public function getRoot(): ?int { return $this->root; } - public function getLeft() + public function getLeft(): ?int { return $this->lft; } - public function getRight() + public function getRight(): ?int { return $this->rgt; } - public function getLevel() + public function getLevel(): ?int { return $this->level; } + + /** + * @return Collection + */ + public function getChildren(): Collection + { + return $this->children; + } + + /** + * @param Collection $children + */ + public function setChildren(Collection $children): void + { + $this->children = $children; + } + + public function setSibling(Node $node): void + { + $this->sibling = $node; + } + + public function getSibling(): ?Node + { + return $this->sibling; + } } diff --git a/tests/Gedmo/Tree/Fixture/Transport/Bus.php b/tests/Gedmo/Tree/Fixture/Transport/Bus.php index 3abc6307a8..45466f0c26 100644 --- a/tests/Gedmo/Tree/Fixture/Transport/Bus.php +++ b/tests/Gedmo/Tree/Fixture/Transport/Bus.php @@ -1,12 +1,22 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Transport; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Bus extends Vehicle { } diff --git a/tests/Gedmo/Tree/Fixture/Transport/Car.php b/tests/Gedmo/Tree/Fixture/Transport/Car.php index 781d5cc6af..69b8a0a01e 100644 --- a/tests/Gedmo/Tree/Fixture/Transport/Car.php +++ b/tests/Gedmo/Tree/Fixture/Transport/Car.php @@ -1,85 +1,136 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Transport; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @Gedmo\Tree(type="nested") + * * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[Gedmo\Tree(type: 'nested')] class Car extends Vehicle { + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="Car", mappedBy="parent") + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + protected $children; + /** * @Gedmo\TreeParent + * * @ORM\ManyToOne(targetEntity="Car", inversedBy="children") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") * }) */ - private $parent; - - /** - * @ORM\OneToMany(targetEntity="Car", mappedBy="parent") - */ - private $children; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?Car $parent = null; /** + * @var int|null + * * @Gedmo\TreeLeft + * * @ORM\Column(type="integer", nullable=true) */ + #[ORM\Column(type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLeft] private $lft; /** + * @var int|null + * * @Gedmo\TreeRight + * * @ORM\Column(type="integer", nullable=true) */ + #[ORM\Column(type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeRight] private $rgt; /** + * @var int|null + * * @Gedmo\TreeRoot + * * @ORM\Column(type="integer", nullable=true) */ + #[ORM\Column(type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeRoot] private $root; /** + * @var int|null + * * @Gedmo\TreeLevel + * * @ORM\Column(name="lvl", type="integer", nullable=true) */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLevel] private $classLevel; - public function setParent($parent = null) + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function setParent(?self $parent = null): void { $this->parent = $parent; } - public function getChildren() + /** + * @return Collection + */ + public function getChildren(): Collection { return $this->children; } - public function getParent() + public function getParent(): ?self { return $this->parent; } - public function getRoot() + public function getRoot(): ?int { return $this->root; } - public function getLeft() + public function getLeft(): ?int { return $this->lft; } - public function getRight() + public function getRight(): ?int { return $this->rgt; } - public function getClassLevel() + public function getClassLevel(): ?int { return $this->classLevel; } diff --git a/tests/Gedmo/Tree/Fixture/Transport/Engine.php b/tests/Gedmo/Tree/Fixture/Transport/Engine.php index 27fb1bd7d0..e13cf0bb23 100644 --- a/tests/Gedmo/Tree/Fixture/Transport/Engine.php +++ b/tests/Gedmo/Tree/Fixture/Transport/Engine.php @@ -1,52 +1,70 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Transport; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Engine { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=32) */ - private $type; + #[ORM\Column(length: 32)] + private ?string $type = null; /** * @ORM\Column(type="integer") */ - private $valves; + #[ORM\Column(type: Types::INTEGER)] + private ?int $valves = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setType($type) + public function setType(?string $type): void { $this->type = $type; } - public function getType() + public function getType(): ?string { return $this->type; } - public function setValves($valves) + public function setValves(?int $valves): void { $this->valves = $valves; } - public function getValves() + public function getValves(): ?int { return $this->valves; } diff --git a/tests/Gedmo/Tree/Fixture/Transport/Vehicle.php b/tests/Gedmo/Tree/Fixture/Transport/Vehicle.php index ec06cb4ff1..f3aa0d9100 100644 --- a/tests/Gedmo/Tree/Fixture/Transport/Vehicle.php +++ b/tests/Gedmo/Tree/Fixture/Transport/Vehicle.php @@ -1,7 +1,17 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture\Transport; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** @@ -9,51 +19,62 @@ * @ORM\InheritanceType("SINGLE_TABLE") * @ORM\DiscriminatorColumn(name="discriminator", type="string") * @ORM\DiscriminatorMap({ - * "vehicle" = "Vehicle", - * "car" = "Car", - * "bus" = "Bus" + * "vehicle": "Vehicle", + * "car": "Car", + * "bus": "Bus" * }) */ +#[ORM\Entity] +#[ORM\InheritanceType('SINGLE_TABLE')] +#[ORM\DiscriminatorColumn(name: 'discriminator', type: Types::STRING)] +#[ORM\DiscriminatorMap(['vehicle' => Vehicle::class, 'car' => Car::class, 'bus' => Bus::class])] class Vehicle { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\OneToOne(targetEntity="Engine") */ - private $engine; + #[ORM\OneToOne(targetEntity: Engine::class)] + private ?Engine $engine = null; /** * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(type: Types::STRING)] + private ?string $title = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setEngine(Engine $engine) + public function setEngine(Engine $engine): void { $this->engine = $engine; } - public function getEngine() + public function getEngine(): ?Engine { return $this->engine; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Tree/Fixture/User.php b/tests/Gedmo/Tree/Fixture/User.php index 392595af8d..1cb526462a 100644 --- a/tests/Gedmo/Tree/Fixture/User.php +++ b/tests/Gedmo/Tree/Fixture/User.php @@ -1,139 +1,122 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") * @ORM\Table(name="user") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[ORM\Table(name: 'user')] class User extends Role { - const PASSWORD_SALT = 'dfJko$~346958rg!DFT]AEtzserf9giq)3/TAeg;aDFa43'; - - /** - * @ORM\Column(name="email", type="string", unique=true) - * @var string - */ - private $email; - - /** - * @ORM\Column(name="password_hash", type="string", length=32) - * @var string - */ - private $passwordHash; - - /** - * @ORM\Column(name="activation_code", type="string", length=12) - * @var string - */ - private $activationCode; - - /** - * @param string $email - * @param string $password - */ - public function __construct($email, $password) - { - parent::__construct(); - $this - ->setEmail($email) - ->setPassword($password); - } - - public function init() + private const PASSWORD_SALT = 'dfJko$~346958rg!DFT]AEtzserf9giq)3/TAeg;aDFa43'; + + /** + * @ORM\Column(name="email", type="string", unique=true) + */ + #[ORM\Column(name: 'email', type: Types::STRING, unique: true)] + private ?string $email = null; + + /** + * @ORM\Column(name="password_hash", type="string", length=32) + */ + #[ORM\Column(name: 'password_hash', type: Types::STRING, length: 32)] + private string $passwordHash; + + /** + * @ORM\Column(name="activation_code", type="string", length=12) + */ + #[ORM\Column(name: 'activation_code', type: Types::STRING, length: 12)] + private ?string $activationCode = null; + + public function __construct(string $email, string $password) + { + parent::__construct(); + $this + ->setEmail($email) + ->setPassword($password); + } + + public function init(): void { $this->setActivationCode($this->generateString(12)); } /** - * Generates a random password - * - * @param int $length - * @return string - */ - public function generateString($length = 8) - { - $length = (int) $length; - if ($length < 0) { - throw new \Exception("Invalid password length '$length'"); - } - $set = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - $num = strlen($set); - $ret = ''; - for ($i = 0; $i < $length; $i++) { - $ret .= $set[rand(0, $num - 1)]; - } - - return $ret; - } - - /** - * Generates a password hash - * - * @param string $password - * @return string - */ - public function generatePasswordHash($password) - { - return md5($password.self::PASSWORD_SALT); - } - - /** - * @return string - */ - public function getEmail() - { - return $this->email; - } - - /** - * @param string $email - * @return User - */ - public function setEmail($email) - { - $this->email = $email; - $this->setRoleId($email); - - return $this; - } - - /** - * @return string - */ - public function getPasswordHash() - { - return $this->passwordHash; - } - - /** - * @param string $password - * @return User - */ - public function setPassword($password) - { - $this->passwordHash = $this->generatePasswordHash(trim($password)); - - return $this; - } - - /** - * @return string - */ - public function getActivationCode() - { - return $this->activationCode; - } - - /** - * @param string $activationCode - * @return User - */ - public function setActivationCode($activationCode) - { - $this->activationCode = $activationCode; - - return $this; - } + * Generates a random password + */ + public function generateString(int $length = 8): string + { + $length = $length; + if ($length < 0) { + throw new \Exception("Invalid password length '$length'"); + } + $set = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $num = strlen($set); + $ret = ''; + for ($i = 0; $i < $length; ++$i) { + $ret .= $set[random_int(0, $num - 1)]; + } + + return $ret; + } + + /** + * Generates a password hash + */ + public function generatePasswordHash(string $password): string + { + return md5($password.self::PASSWORD_SALT); + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + $this->setRoleId($email); + + return $this; + } + + public function getPasswordHash(): string + { + return $this->passwordHash; + } + + public function setPassword(string $password): self + { + $this->passwordHash = $this->generatePasswordHash(trim($password)); + + return $this; + } + + public function getActivationCode(): ?string + { + return $this->activationCode; + } + + public function setActivationCode(string $activationCode): self + { + $this->activationCode = $activationCode; + + return $this; + } } diff --git a/tests/Gedmo/Tree/Fixture/UserGroup.php b/tests/Gedmo/Tree/Fixture/UserGroup.php index 265089ca4f..737711851a 100644 --- a/tests/Gedmo/Tree/Fixture/UserGroup.php +++ b/tests/Gedmo/Tree/Fixture/UserGroup.php @@ -1,7 +1,19 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * Group entity @@ -9,33 +21,32 @@ * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") * @ORM\Table(name="user_group") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[ORM\Table(name: 'user_group')] class UserGroup extends Role { /** - * @ORM\Column(name="name", type="string", length=255) - * @var string - */ - private $name; + * @ORM\Column(name="name", type="string", length=191) + */ + #[ORM\Column(name: 'name', type: Types::STRING, length: 191)] + private string $name; - public function __construct($name) + public function __construct(string $name) { $this->setName($name); } - /** - * @return string - */ - public function getRoleId() - { - return $this->name; - } + public function getRoleId(): ?string + { + return $this->name; + } - public function getName() + public function getName(): string { return $this->name; } - public function setName($name) + public function setName(string $name): self { $this->name = $name; $this->setRoleId($name); diff --git a/tests/Gedmo/Tree/Fixture/UserLDAP.php b/tests/Gedmo/Tree/Fixture/UserLDAP.php index f9496d241e..156034c0fb 100644 --- a/tests/Gedmo/Tree/Fixture/UserLDAP.php +++ b/tests/Gedmo/Tree/Fixture/UserLDAP.php @@ -1,16 +1,29 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; /** * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") * @ORM\Table(name="user_ldap") */ +#[ORM\Entity(repositoryClass: NestedTreeRepository::class)] +#[ORM\Table(name: 'user_ldap')] class UserLDAP extends User { - public function __construct($ldapUserName) + public function __construct(string $ldapUserName = 'next@something.com') { - parent::__construct('next@something.com', 'pass'); + parent::__construct($ldapUserName, 'pass'); } } diff --git a/tests/Gedmo/Tree/InMemoryUpdatesTest.php b/tests/Gedmo/Tree/InMemoryUpdatesTest.php index 677373ff94..1d9536b388 100644 --- a/tests/Gedmo/Tree/InMemoryUpdatesTest.php +++ b/tests/Gedmo/Tree/InMemoryUpdatesTest.php @@ -1,48 +1,54 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\Category; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Category; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class InMemoryUpdatesTest extends BaseTestCaseORM +final class InMemoryUpdatesTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\Category"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testInMemoryTreeInserts() + public function testInMemoryTreeInserts(): void { - $meta = $this->em->getClassMetadata(self::CATEGORY); - $repo = $this->em->getRepository(self::CATEGORY); + $meta = $this->em->getClassMetadata(Category::class); + $repo = $this->em->getRepository(Category::class); $root = new Category(); $this->em->persist($root); - $root->setTitle("Root"); + $root->setTitle('Root'); $child = new Category(); $this->em->persist($child); - $child->setTitle("child"); + $child->setTitle('child'); $child2 = new Category(); $this->em->persist($child2); - $child2->setTitle("child2"); + $child2->setTitle('child2'); $child2->setParent($root); $child->setParent($root); @@ -51,7 +57,7 @@ public function testInMemoryTreeInserts() $childsChild = new Category(); $this->em->persist($childsChild); - $childsChild->setTitle("childs_child"); + $childsChild->setTitle('childs_child'); $childsChild->setParent($child); $this->em->flush(); @@ -60,24 +66,24 @@ public function testInMemoryTreeInserts() $node = $repo->find(2); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(2, $left); - $this->assertEquals(5, $right); + static::assertSame(2, $left); + static::assertSame(5, $right); $node = $repo->find(3); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(6, $left); - $this->assertEquals(7, $right); + static::assertSame(6, $left); + static::assertSame(7, $right); $node = $repo->find(4); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(3, $left); - $this->assertEquals(4, $right); + static::assertSame(3, $left); + static::assertSame(4, $right); /*print "Tree:\n"; for ($i=1; $i < 5; $i++) { - $node = $this->em->getRepository(self::CATEGORY)->find($i); + $node = $this->em->getRepository(Category::class)->find($i); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); $level = $meta->getReflectionProperty('level')->getValue($node); @@ -86,10 +92,10 @@ public function testInMemoryTreeInserts() print "\n\n";*/ } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CATEGORY, - ); + return [ + Category::class, + ]; } } diff --git a/tests/Gedmo/Tree/InMemoryUpdatesWithInheritanceTest.php b/tests/Gedmo/Tree/InMemoryUpdatesWithInheritanceTest.php index 4ac137d297..27a5b2e72d 100644 --- a/tests/Gedmo/Tree/InMemoryUpdatesWithInheritanceTest.php +++ b/tests/Gedmo/Tree/InMemoryUpdatesWithInheritanceTest.php @@ -1,39 +1,42 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\Genealogy\Man; -use Tree\Fixture\Genealogy\Woman; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Genealogy\Man; +use Gedmo\Tests\Tree\Fixture\Genealogy\Person; +use Gedmo\Tests\Tree\Fixture\Genealogy\Woman; +use Gedmo\Tree\TreeListener; /** * Additional tests for tree inheritance and in-memory updates * * @author Illya Klymov - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class InMemoryUpdatesWithInheritanceTest extends BaseTestCaseORM +final class InMemoryUpdatesWithInheritanceTest extends BaseTestCaseORM { - const PERSON = "Tree\\Fixture\\Genealogy\\Person"; - const MAN = "Tree\\Fixture\\Genealogy\\Man"; - const WOMAN = "Tree\\Fixture\\Genealogy\\Woman"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testInMemoryTreeInsertsWithInheritance() + public function testInMemoryTreeInsertsWithInheritance(): void { - $nodes = array(); - $man1 = new Man('Root - Man1'); $this->em->persist($man1); @@ -54,38 +57,38 @@ public function testInMemoryTreeInsertsWithInheritance() $left = $man1->getLeft(); $right = $man1->getRight(); $level = $man1->getLevel(); - $this->assertEquals(1, $left); - $this->assertEquals(8, $right); - $this->assertEquals(0, $level); + static::assertSame(1, $left); + static::assertSame(8, $right); + static::assertSame(0, $level); $left = $woman1->getLeft(); $right = $woman1->getRight(); $level = $woman1->getLevel(); - $this->assertEquals(2, $left); - $this->assertEquals(7, $right); - $this->assertEquals(1, $level); + static::assertSame(2, $left); + static::assertSame(7, $right); + static::assertSame(1, $level); $left = $man2->getLeft(); $right = $man2->getRight(); $level = $man2->getLevel(); - $this->assertEquals(3, $left); - $this->assertEquals(6, $right); - $this->assertEquals(2, $level); + static::assertSame(3, $left); + static::assertSame(6, $right); + static::assertSame(2, $level); $left = $woman2->getLeft(); $right = $woman2->getRight(); $level = $woman2->getLevel(); - $this->assertEquals(4, $left); - $this->assertEquals(5, $right); - $this->assertEquals(3, $level); + static::assertSame(4, $left); + static::assertSame(5, $right); + static::assertSame(3, $level); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::PERSON, - self::MAN, - self::WOMAN, - ); + return [ + Person::class, + Man::class, + Woman::class, + ]; } } diff --git a/tests/Gedmo/Tree/Issue/Issue2408Test.php b/tests/Gedmo/Tree/Issue/Issue2408Test.php new file mode 100644 index 0000000000..a79c25babf --- /dev/null +++ b/tests/Gedmo/Tree/Issue/Issue2408Test.php @@ -0,0 +1,74 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Issue; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Issue2408\Category; +use Gedmo\Tree\TreeListener; + +final class Issue2408Test extends BaseTestCaseORM +{ + private TreeListener $listener; + + protected function setUp(): void + { + parent::setUp(); + + $this->listener = new TreeListener(); + + $evm = new EventManager(); + $evm->addEventSubscriber($this->listener); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testSettingParentNull(): void + { + $food = new Category(); + $food->setTitle('Food'); + + $fruits = new Category(); + $fruits->setTitle('Fruits'); + $fruits->setParent($food); + + $vegetables = new Category(); + $vegetables->setTitle('Vegetables'); + $vegetables->setParent($food); + + $carrots = new Category(); + $carrots->setTitle('Carrots'); + $carrots->setParent($vegetables); + + $this->em->persist($food); + $this->em->persist($fruits); + $this->em->persist($vegetables); + $this->em->persist($carrots); + $this->em->flush(); + + $this->em->refresh($carrots); + + $carrots->setParent(null); + $this->em->flush(); + + $categoryRepository = $this->em->getRepository(Category::class); + $verify = $categoryRepository->verify(); + + static::assertTrue($verify); + static::assertSame($carrots, $carrots->getRoot()); + } + + protected function getUsedEntityFixtures(): array + { + return [Category::class]; + } +} diff --git a/tests/Gedmo/Tree/Issue/Issue2517Test.php b/tests/Gedmo/Tree/Issue/Issue2517Test.php new file mode 100644 index 0000000000..8084517811 --- /dev/null +++ b/tests/Gedmo/Tree/Issue/Issue2517Test.php @@ -0,0 +1,65 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Issue; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Issue2517\Category; +use Gedmo\Tree\TreeListener; + +final class Issue2517Test extends BaseTestCaseORM +{ + private TreeListener $listener; + + protected function setUp(): void + { + parent::setUp(); + + $this->listener = new TreeListener(); + + $evm = new EventManager(); + $evm->addEventSubscriber($this->listener); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testGetNextSiblingsWithoutIdentifierMethod(): void + { + $food = new Category(); + $food->setTitle('Food'); + + $fruits = new Category(); + $fruits->setTitle('Fruits'); + $fruits->setParent($food); + + $vegetables = new Category(); + $vegetables->setTitle('Vegetables'); + $vegetables->setParent($food); + + $this->em->persist($food); + $this->em->persist($fruits); + $this->em->persist($vegetables); + $this->em->flush(); + + $categoryRepository = $this->em->getRepository(Category::class); + + static::assertTrue($categoryRepository->verify()); + static::assertCount(0, $categoryRepository->getNextSiblings($food)); + static::assertCount(1, $categoryRepository->getNextSiblings($fruits)); + static::assertCount(0, $categoryRepository->getNextSiblings($vegetables)); + } + + protected function getUsedEntityFixtures(): array + { + return [Category::class]; + } +} diff --git a/tests/Gedmo/Tree/Issue/Issue2616Test.php b/tests/Gedmo/Tree/Issue/Issue2616Test.php new file mode 100644 index 0000000000..788be2f705 --- /dev/null +++ b/tests/Gedmo/Tree/Issue/Issue2616Test.php @@ -0,0 +1,81 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Issue; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Issue2616\Category; +use Gedmo\Tests\Tree\Fixture\Issue2616\Page; +use Gedmo\Tree\TreeListener; + +class Issue2616Test extends BaseTestCaseORM +{ + private TreeListener $listener; + + protected function setUp(): void + { + parent::setUp(); + + $this->listener = new TreeListener(); + + $evm = new EventManager(); + $evm->addEventSubscriber($this->listener); + + $this->getDefaultMockSqliteEntityManager($evm); + } + + public function testGetNextSiblingsWithoutIdentifierMethod(): void + { + $food = new Category(); + $food->setTitle('Food'); + + $fruits = new Category(); + $fruits->setTitle('Fruits'); + $fruits->setParent($food); + + $page1 = new Page(); + $page1->setTitle('Page 1'); + $fruits->setPage($page1); + + $vegetables = new Category(); + $vegetables->setTitle('Vegetables'); + $vegetables->setParent($food); + + $page2 = new Page(); + $page2->setTitle('Page 2'); + $vegetables->setPage($page2); + + $this->em->persist($food); + $this->em->persist($fruits); + $this->em->persist($vegetables); + $this->em->persist($page1); + $this->em->persist($page2); + $this->em->flush(); + + $this->em->clear(); + + $categoryRepo = $this->em->getRepository(Category::class); + $food = $categoryRepo->findOneBy(['title' => 'Food']); + + $this->em->remove($food); + $this->em->flush(); + + static::assertNull($categoryRepo->findOneBy(['title' => 'Fruits'])); + static::assertNull($categoryRepo->findOneBy(['title' => 'Vegetables'])); + + // Page should be removed as well, because children Fruits/Vegetables are removed and they have Page with cascade remove. + static::assertEmpty($this->em->getRepository(Page::class)->findAll()); + } + + protected function getUsedEntityFixtures(): array + { + return [Category::class, Page::class]; + } +} diff --git a/tests/Gedmo/Tree/MaterializedPathODMMongoDBRepositoryTest.php b/tests/Gedmo/Tree/MaterializedPathODMMongoDBRepositoryTest.php index c28c4d8581..026f45e8a7 100644 --- a/tests/Gedmo/Tree/MaterializedPathODMMongoDBRepositoryTest.php +++ b/tests/Gedmo/Tree/MaterializedPathODMMongoDBRepositoryTest.php @@ -1,293 +1,337 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseMongoODM; +use Doctrine\ODM\MongoDB\Iterator\Iterator; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; +use Gedmo\Tests\Tree\Fixture\Document\Category; +use Gedmo\Tree\Document\MongoDB\Repository\MaterializedPathRepository; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MaterializedPathODMMongoDBRepositoryTest extends BaseTestCaseMongoODM +final class MaterializedPathODMMongoDBRepositoryTest extends BaseTestCaseMongoODM { - const CATEGORY = "Tree\\Fixture\\Document\\Category"; - /** @var $this->repo \Gedmo\Tree\Document\MongoDB\Repository\MaterializedPathRepository */ - protected $repo; + /** + * @var MaterializedPathRepository + */ + private MaterializedPathRepository $repo; - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); $this->populate(); - $this->repo = $this->dm->getRepository(self::CATEGORY); + $this->repo = $this->dm->getRepository(Category::class); } - /** - * @test - */ - public function getRootNodes() + public function testGetRootNodes(): void { $result = $this->repo->getRootNodes('title'); - $this->assertEquals(3, $result->count()); - $this->assertEquals('Drinks', $result->getNext()->getTitle()); - $this->assertEquals('Food', $result->getNext()->getTitle()); - $this->assertEquals('Sports', $result->getNext()->getTitle()); + static::assertInstanceOf(Iterator::class, $result); + static::assertSame(3, \iterator_count($result)); + $result->rewind(); + + static::assertSame('Drinks', $result->current()->getTitle()); + $result->next(); + static::assertSame('Food', $result->current()->getTitle()); + $result->next(); + static::assertSame('Sports', $result->current()->getTitle()); } - /** - * @test - */ - public function getChildren() + public function testGetChildren(): void { - $root = $this->repo->findOneByTitle('Food'); + $root = $this->repo->findOneBy(['title' => 'Food']); // Get all children from the root, including it $result = $this->repo->getChildren($root, false, 'title', 'asc', true); - $this->assertEquals(5, count($result)); - $this->assertEquals('Carrots', $result->getNext()->getTitle()); - $this->assertEquals('Food', $result->getNext()->getTitle()); - $this->assertEquals('Fruits', $result->getNext()->getTitle()); - $this->assertEquals('Potatoes', $result->getNext()->getTitle()); - $this->assertEquals('Vegitables', $result->getNext()->getTitle()); + static::assertInstanceOf(Iterator::class, $result); + static::assertSame(5, \iterator_count($result)); + $result->rewind(); + + static::assertSame('Carrots', $result->current()->getTitle()); + $result->next(); + static::assertSame('Food', $result->current()->getTitle()); + $result->next(); + static::assertSame('Fruits', $result->current()->getTitle()); + $result->next(); + static::assertSame('Potatoes', $result->current()->getTitle()); + $result->next(); + static::assertSame('Vegitables', $result->current()->getTitle()); // Get all children from the root, NOT including it $result = $this->repo->getChildren($root, false, 'title', 'asc', false); - $this->assertEquals(4, count($result)); - $this->assertEquals('Carrots', $result->getNext()->getTitle()); - $this->assertEquals('Fruits', $result->getNext()->getTitle()); - $this->assertEquals('Potatoes', $result->getNext()->getTitle()); - $this->assertEquals('Vegitables', $result->getNext()->getTitle()); + static::assertInstanceOf(Iterator::class, $result); + static::assertSame(4, \iterator_count($result)); + $result->rewind(); + static::assertSame('Carrots', $result->current()->getTitle()); + $result->next(); + static::assertSame('Fruits', $result->current()->getTitle()); + $result->next(); + static::assertSame('Potatoes', $result->current()->getTitle()); + $result->next(); + static::assertSame('Vegitables', $result->current()->getTitle()); // Get direct children from the root, including it $result = $this->repo->getChildren($root, true, 'title', 'asc', true); - $this->assertEquals(3, $result->count()); - $this->assertEquals('Food', $result->getNext()->getTitle()); - $this->assertEquals('Fruits', $result->getNext()->getTitle()); - $this->assertEquals('Vegitables', $result->getNext()->getTitle()); + static::assertInstanceOf(Iterator::class, $result); + static::assertSame(3, \iterator_count($result)); + $result->rewind(); + static::assertSame('Food', $result->current()->getTitle()); + $result->next(); + static::assertSame('Fruits', $result->current()->getTitle()); + $result->next(); + static::assertSame('Vegitables', $result->current()->getTitle()); // Get direct children from the root, NOT including it $result = $this->repo->getChildren($root, true, 'title', 'asc', false); + static::assertInstanceOf(Iterator::class, $result); - $this->assertEquals(2, $result->count()); - $this->assertEquals('Fruits', $result->getNext()->getTitle()); - $this->assertEquals('Vegitables', $result->getNext()->getTitle()); + static::assertSame(2, \iterator_count($result)); + $result->rewind(); + static::assertSame('Fruits', $result->current()->getTitle()); + $result->next(); + static::assertSame('Vegitables', $result->current()->getTitle()); // Get ALL nodes $result = $this->repo->getChildren(null, false, 'title'); - - $this->assertEquals(9, $result->count()); - $this->assertEquals('Best Whisky', $result->getNext()->getTitle()); - $this->assertEquals('Carrots', $result->getNext()->getTitle()); - $this->assertEquals('Drinks', $result->getNext()->getTitle()); - $this->assertEquals('Food', $result->getNext()->getTitle()); - $this->assertEquals('Fruits', $result->getNext()->getTitle()); - $this->assertEquals('Potatoes', $result->getNext()->getTitle()); - $this->assertEquals('Sports', $result->getNext()->getTitle()); - $this->assertEquals('Vegitables', $result->getNext()->getTitle()); - $this->assertEquals('Whisky', $result->getNext()->getTitle()); + static::assertInstanceOf(Iterator::class, $result); + + static::assertSame(9, \iterator_count($result)); + $result->rewind(); + static::assertSame('Best Whisky', $result->current()->getTitle()); + $result->next(); + static::assertSame('Carrots', $result->current()->getTitle()); + $result->next(); + static::assertSame('Drinks', $result->current()->getTitle()); + $result->next(); + static::assertSame('Food', $result->current()->getTitle()); + $result->next(); + static::assertSame('Fruits', $result->current()->getTitle()); + $result->next(); + static::assertSame('Potatoes', $result->current()->getTitle()); + $result->next(); + static::assertSame('Sports', $result->current()->getTitle()); + $result->next(); + static::assertSame('Vegitables', $result->current()->getTitle()); + $result->next(); + static::assertSame('Whisky', $result->current()->getTitle()); // Get ALL root nodes $result = $this->repo->getChildren(null, true, 'title'); - - $this->assertEquals(3, $result->count()); - $this->assertEquals('Drinks', $result->getNext()->getTitle()); - $this->assertEquals('Food', $result->getNext()->getTitle()); - $this->assertEquals('Sports', $result->getNext()->getTitle()); + static::assertInstanceOf(Iterator::class, $result); + + static::assertSame(3, \iterator_count($result)); + $result->rewind(); + static::assertSame('Drinks', $result->current()->getTitle()); + $result->next(); + static::assertSame('Food', $result->current()->getTitle()); + $result->next(); + static::assertSame('Sports', $result->current()->getTitle()); } - /** - * @test - */ - public function getTree() + public function testGetTree(): void { $tree = $this->repo->getTree(); - $this->assertEquals(9, $tree->count()); - $this->assertEquals('Drinks', $tree->getNext()->getTitle()); - $this->assertEquals('Whisky', $tree->getNext()->getTitle()); - $this->assertEquals('Best Whisky', $tree->getNext()->getTitle()); - $this->assertEquals('Food', $tree->getNext()->getTitle()); - $this->assertEquals('Fruits', $tree->getNext()->getTitle()); - $this->assertEquals('Vegitables', $tree->getNext()->getTitle()); - $this->assertEquals('Carrots', $tree->getNext()->getTitle()); - $this->assertEquals('Potatoes', $tree->getNext()->getTitle()); - $this->assertEquals('Sports', $tree->getNext()->getTitle()); + static::assertSame(9, \iterator_count($tree)); + $tree->rewind(); + static::assertSame('Drinks', $tree->current()->getTitle()); + $tree->next(); + static::assertSame('Whisky', $tree->current()->getTitle()); + $tree->next(); + static::assertSame('Best Whisky', $tree->current()->getTitle()); + $tree->next(); + static::assertSame('Food', $tree->current()->getTitle()); + $tree->next(); + static::assertSame('Fruits', $tree->current()->getTitle()); + $tree->next(); + static::assertSame('Vegitables', $tree->current()->getTitle()); + $tree->next(); + static::assertSame('Carrots', $tree->current()->getTitle()); + $tree->next(); + static::assertSame('Potatoes', $tree->current()->getTitle()); + $tree->next(); + static::assertSame('Sports', $tree->current()->getTitle()); // Get a specific tree $roots = $this->repo->getRootNodes(); - $tree = $this->repo->getTree($roots->getNext()); - - $this->assertEquals(3, $tree->count()); - $this->assertEquals('Drinks', $tree->getNext()->getTitle()); - $this->assertEquals('Whisky', $tree->getNext()->getTitle()); - $this->assertEquals('Best Whisky', $tree->getNext()->getTitle()); + static::assertInstanceOf(Iterator::class, $roots); + $tree = $this->repo->getTree($roots->current()); + + static::assertSame(3, \iterator_count($tree)); + $tree->rewind(); + static::assertSame('Drinks', $tree->current()->getTitle()); + $tree->next(); + static::assertSame('Whisky', $tree->current()->getTitle()); + $tree->next(); + static::assertSame('Best Whisky', $tree->current()->getTitle()); } - /** - * @test - */ - public function childrenHierarchy() + public function testChildrenHierarchy(): void { $tree = $this->repo->childrenHierarchy(); - $this->assertEquals('Drinks', $tree[0]['title']); - $this->assertEquals('Whisky', $tree[0]['__children'][0]['title']); - $this->assertEquals('Best Whisky', $tree[0]['__children'][0]['__children'][0]['title']); + static::assertSame('Drinks', $tree[0]['title']); + static::assertSame('Whisky', $tree[0]['__children'][0]['title']); + static::assertSame('Best Whisky', $tree[0]['__children'][0]['__children'][0]['title']); $vegitablesChildren = $tree[1]['__children'][1]['__children']; - $this->assertEquals('Food', $tree[1]['title']); - $this->assertEquals('Fruits', $tree[1]['__children'][0]['title']); - $this->assertEquals('Vegitables', $tree[1]['__children'][1]['title']); - $this->assertEquals('Carrots', $vegitablesChildren[0]['title']); - $this->assertEquals('Potatoes', $vegitablesChildren[1]['title']); - $this->assertEquals('Sports', $tree[2]['title']); + static::assertSame('Food', $tree[1]['title']); + static::assertSame('Fruits', $tree[1]['__children'][0]['title']); + static::assertSame('Vegitables', $tree[1]['__children'][1]['title']); + static::assertSame('Carrots', $vegitablesChildren[0]['title']); + static::assertSame('Potatoes', $vegitablesChildren[1]['title']); + static::assertSame('Sports', $tree[2]['title']); // Tree of one specific root $roots = $this->repo->getRootNodes(); - $drinks = $roots->getNext(); - $food = $roots->getNext(); + static::assertInstanceOf(Iterator::class, $roots); + $drinks = $roots->current(); + $roots->next(); + $food = $roots->current(); + $roots->next(); $tree = $this->repo->childrenHierarchy(); - $this->assertEquals('Drinks', $tree[0]['title']); - $this->assertEquals('Whisky', $tree[0]['__children'][0]['title']); - $this->assertEquals('Best Whisky', $tree[0]['__children'][0]['__children'][0]['title']); + static::assertSame('Drinks', $tree[0]['title']); + static::assertSame('Whisky', $tree[0]['__children'][0]['title']); + static::assertSame('Best Whisky', $tree[0]['__children'][0]['__children'][0]['title']); // Tree of one specific root, with the root node - $tree = $this->repo->childrenHierarchy($drinks, false, array(), true); + $tree = $this->repo->childrenHierarchy($drinks, false, [], true); - $this->assertEquals('Drinks', $tree[0]['title']); - $this->assertEquals('Whisky', $tree[0]['__children'][0]['title']); - $this->assertEquals('Best Whisky', $tree[0]['__children'][0]['__children'][0]['title']); + static::assertSame('Drinks', $tree[0]['title']); + static::assertSame('Whisky', $tree[0]['__children'][0]['title']); + static::assertSame('Best Whisky', $tree[0]['__children'][0]['__children'][0]['title']); // Tree of one specific root only with direct children, without the root node $roots = $this->repo->getRootNodes(); $tree = $this->repo->childrenHierarchy($food, true); - $this->assertEquals(2, count($tree)); - $this->assertEquals('Fruits', $tree[0]['title']); - $this->assertEquals('Vegitables', $tree[1]['title']); + static::assertCount(2, $tree); + static::assertSame('Fruits', $tree[0]['title']); + static::assertSame('Vegitables', $tree[1]['title']); // Tree of one specific root only with direct children, with the root node - $tree = $this->repo->childrenHierarchy($food, true, array(), true); + $tree = $this->repo->childrenHierarchy($food, true, [], true); - $this->assertEquals(1, count($tree)); - $this->assertEquals(2, count($tree[0]['__children'])); - $this->assertEquals('Food', $tree[0]['title']); - $this->assertEquals('Fruits', $tree[0]['__children'][0]['title']); - $this->assertEquals('Vegitables', $tree[0]['__children'][1]['title']); + static::assertCount(1, $tree); + static::assertCount(2, $tree[0]['__children']); + static::assertSame('Food', $tree[0]['title']); + static::assertSame('Fruits', $tree[0]['__children'][0]['title']); + static::assertSame('Vegitables', $tree[0]['__children'][1]['title']); // HTML Tree of one specific root, without the root node $roots = $this->repo->getRootNodes(); - $tree = $this->repo->childrenHierarchy($drinks, false, array('decorate' => true), false); + $tree = $this->repo->childrenHierarchy($drinks, false, ['decorate' => true], false); - $this->assertEquals('
            • Whisky
              • Best Whisky
            ', $tree); + static::assertSame('
            • Whisky
              • Best Whisky
            ', $tree); // HTML Tree of one specific root, with the root node $roots = $this->repo->getRootNodes(); - $tree = $this->repo->childrenHierarchy($drinks, false, array('decorate' => true), true); + $tree = $this->repo->childrenHierarchy($drinks, false, ['decorate' => true], true); - $this->assertEquals('
            • Drinks
              • Whisky
                • Best Whisky
            ', $tree); + static::assertSame('
            • Drinks
              • Whisky
                • Best Whisky
            ', $tree); } - public function testChildCount() + public function testChildCount(): void { // Count all $count = $this->repo->childCount(); - $this->assertEquals(9, $count); + static::assertSame(9, $count); // Count all, but only direct ones $count = $this->repo->childCount(null, true); - $this->assertEquals(3, $count); + static::assertSame(3, $count); // Count food children - $food = $this->repo->findOneByTitle('Food'); + $food = $this->repo->findOneBy(['title' => 'Food']); $count = $this->repo->childCount($food); - $this->assertEquals(4, $count); + static::assertSame(4, $count); // Count food children, but only direct ones $count = $this->repo->childCount($food, true); - $this->assertEquals(2, $count); + static::assertSame(2, $count); } - /** - * @expectedException \Gedmo\Exception\InvalidArgumentException - */ - public function testChildCount_ifAnObjectIsPassedWhichIsNotAnInstanceOfTheEntityClassThrowException() + public function testChildCountIfAnObjectIsPassedWhichIsNotAnInstanceOfTheEntityClassThrowException(): void { + $this->expectException(InvalidArgumentException::class); $this->repo->childCount(new \DateTime()); } - /** - * @expectedException \Gedmo\Exception\InvalidArgumentException - */ - public function testChildCount_ifAnObjectIsPassedIsAnInstanceOfTheEntityClassButIsNotHandledByUnitOfWorkThrowException() + public function testChildCountIfAnObjectIsPassedIsAnInstanceOfTheEntityClassButIsNotHandledByUnitOfWorkThrowException(): void { + $this->expectException(InvalidArgumentException::class); $this->repo->childCount($this->createCategory()); } - public function test_changeChildrenIndex() + public function testChangeChildrenIndex(): void { $childrenIndex = 'myChildren'; $this->repo->setChildrenIndex($childrenIndex); $tree = $this->repo->childrenHierarchy(); - $this->assertInternalType('array', $tree[0][$childrenIndex]); - } - - protected function getUsedEntityFixtures() - { - return array( - self::CATEGORY, - ); + static::assertIsArray($tree[0][$childrenIndex]); } - public function createCategory() + private function createCategory(): Category { - $class = self::CATEGORY; + $class = Category::class; return new $class(); } - private function populate() + private function populate(): void { $root = $this->createCategory(); - $root->setTitle("Food"); + $root->setTitle('Food'); $root2 = $this->createCategory(); - $root2->setTitle("Sports"); + $root2->setTitle('Sports'); $child = $this->createCategory(); - $child->setTitle("Fruits"); + $child->setTitle('Fruits'); $child->setParent($root); $child2 = $this->createCategory(); - $child2->setTitle("Vegitables"); + $child2->setTitle('Vegitables'); $child2->setParent($root); $childsChild = $this->createCategory(); - $childsChild->setTitle("Carrots"); + $childsChild->setTitle('Carrots'); $childsChild->setParent($child2); $potatoes = $this->createCategory(); - $potatoes->setTitle("Potatoes"); + $potatoes->setTitle('Potatoes'); $potatoes->setParent($child2); $drinks = $this->createCategory(); diff --git a/tests/Gedmo/Tree/MaterializedPathODMMongoDBTest.php b/tests/Gedmo/Tree/MaterializedPathODMMongoDBTest.php index 0cef505e55..b8da9f6691 100644 --- a/tests/Gedmo/Tree/MaterializedPathODMMongoDBTest.php +++ b/tests/Gedmo/Tree/MaterializedPathODMMongoDBTest.php @@ -1,26 +1,42 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseMongoODM; +use Doctrine\ODM\MongoDB\Iterator\Iterator; +use Gedmo\Exception\RuntimeException; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; +use Gedmo\Tests\Tree\Fixture\Document\Category; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MaterializedPathODMMongoDBTest extends BaseTestCaseMongoODM +final class MaterializedPathODMMongoDBTest extends BaseTestCaseMongoODM { - const CATEGORY = "Tree\\Fixture\\Document\\Category"; - + /** + * @var array + */ protected $config; + + /** + * @var TreeListener + */ protected $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -29,16 +45,13 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($this->listener); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); - $meta = $this->dm->getClassMetadata(self::CATEGORY); - $this->config = $this->listener->getConfiguration($this->dm, $meta->name); + $meta = $this->dm->getClassMetadata(Category::class); + $this->config = $this->listener->getConfiguration($this->dm, $meta->getName()); } - /** - * @test - */ - public function insertUpdateAndRemove() + public function testInsertUpdateAndRemove(): void { // Insert $category = $this->createCategory(); @@ -64,14 +77,14 @@ public function insertUpdateAndRemove() $this->dm->refresh($category3); $this->dm->refresh($category4); - $this->assertEquals($this->generatePath(array('1' => $category->getId())), $category->getPath()); - $this->assertEquals($this->generatePath(array('1' => $category->getId(), '2' => $category2->getId())), $category2->getPath()); - $this->assertEquals($this->generatePath(array('1' => $category->getId(), '2' => $category2->getId(), '3' => $category3->getId())), $category3->getPath()); - $this->assertEquals($this->generatePath(array('4' => $category4->getId())), $category4->getPath()); - $this->assertEquals(1, $category->getLevel()); - $this->assertEquals(2, $category2->getLevel()); - $this->assertEquals(3, $category3->getLevel()); - $this->assertEquals(1, $category4->getLevel()); + static::assertSame($this->generatePath(['1' => $category->getId()]), $category->getPath()); + static::assertSame($this->generatePath(['1' => $category->getId(), '2' => $category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath(['1' => $category->getId(), '2' => $category2->getId(), '3' => $category3->getId()]), $category3->getPath()); + static::assertSame($this->generatePath(['4' => $category4->getId()]), $category4->getPath()); + static::assertSame(1, $category->getLevel()); + static::assertSame(2, $category2->getLevel()); + static::assertSame(3, $category3->getLevel()); + static::assertSame(1, $category4->getLevel()); // Update $category2->setParent(null); @@ -83,34 +96,34 @@ public function insertUpdateAndRemove() $this->dm->refresh($category2); $this->dm->refresh($category3); - $this->assertEquals($this->generatePath(array('1' => $category->getId())), $category->getPath()); - $this->assertEquals($this->generatePath(array('2' => $category2->getId())), $category2->getPath()); - $this->assertEquals($this->generatePath(array('2' => $category2->getId(), '3' => $category3->getId())), $category3->getPath()); - $this->assertEquals(1, $category->getLevel()); - $this->assertEquals(1, $category2->getLevel()); - $this->assertEquals(2, $category3->getLevel()); - $this->assertEquals(1, $category4->getLevel()); + static::assertSame($this->generatePath(['1' => $category->getId()]), $category->getPath()); + static::assertSame($this->generatePath(['2' => $category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath(['2' => $category2->getId(), '3' => $category3->getId()]), $category3->getPath()); + static::assertSame(1, $category->getLevel()); + static::assertSame(1, $category2->getLevel()); + static::assertSame(2, $category3->getLevel()); + static::assertSame(1, $category4->getLevel()); // Remove $this->dm->remove($category); $this->dm->remove($category2); $this->dm->flush(); - $result = $this->dm->createQueryBuilder()->find(self::CATEGORY)->getQuery()->execute(); + $result = $this->dm->createQueryBuilder()->find(Category::class)->getQuery()->getIterator(); + + static::assertInstanceOf(Iterator::class, $result); - $firstResult = $result->getNext(); + /** @var Category $firstResult */ + $firstResult = $result->current(); - $this->assertEquals(1, $result->count()); - $this->assertEquals('4', $firstResult->getTitle()); - $this->assertEquals(1, $firstResult->getLevel()); + static::assertCount(1, $result->toArray()); + static::assertSame('4', $firstResult->getTitle()); + static::assertSame(1, $firstResult->getLevel()); } - /** - * @test - */ - public function useOfSeparatorInPathSourceShouldThrowAnException() + public function testUseOfSeparatorInPathSourceShouldThrowAnException(): void { - $this->setExpectedException('Gedmo\Exception\RuntimeException'); + $this->expectException(RuntimeException::class); $category = $this->createCategory(); $category->setTitle('1'.$this->config['path_separator']); @@ -119,14 +132,17 @@ public function useOfSeparatorInPathSourceShouldThrowAnException() $this->dm->flush(); } - public function createCategory() + private function createCategory(): Category { - $class = self::CATEGORY; + $class = Category::class; return new $class(); } - public function generatePath(array $sources) + /** + * @param array $sources + */ + private function generatePath(array $sources): string { $path = ''; diff --git a/tests/Gedmo/Tree/MaterializedPathODMMongoDBTreeLockingTest.php b/tests/Gedmo/Tree/MaterializedPathODMMongoDBTreeLockingTest.php index d7ded9a030..60e2a9a85d 100644 --- a/tests/Gedmo/Tree/MaterializedPathODMMongoDBTreeLockingTest.php +++ b/tests/Gedmo/Tree/MaterializedPathODMMongoDBTreeLockingTest.php @@ -1,27 +1,41 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseMongoODM; -use Tree\Fixture\Mock\TreeListenerMock; +use Gedmo\Exception\TreeLockingException; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; +use Gedmo\Tests\Tree\Fixture\Document\Article; +use Gedmo\Tests\Tree\Fixture\Mock\TreeListenerMock; /** * These are tests for Tree behavior * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MaterializedPathODMMongoDBTreeLockingTest extends BaseTestCaseMongoODM +final class MaterializedPathODMMongoDBTreeLockingTest extends BaseTestCaseMongoODM { - const ARTICLE = "Tree\\Fixture\\Document\\Article"; - + /** + * @var array + */ protected $config; + + /** + * @var TreeListenerMock + */ protected $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -30,20 +44,17 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($this->listener); - $this->getMockDocumentManager($evm); + $this->getDefaultDocumentManager($evm); - $meta = $this->dm->getClassMetadata(self::ARTICLE); - $this->config = $this->listener->getConfiguration($this->dm, $meta->name); + $meta = $this->dm->getClassMetadata(Article::class); + $this->config = $this->listener->getConfiguration($this->dm, $meta->getName()); } - /** - * @test - */ - public function modifyingANodeWhileItsTreeIsLockedShouldThrowAnException() + public function testModifyingANodeWhileItsTreeIsLockedShouldThrowAnException(): void { // By default, TreeListenerMock disables the release of the locks // for testing purposes - $this->setExpectedException('Gedmo\Exception\TreeLockingException'); + $this->expectException(TreeLockingException::class); $article = $this->createArticle(); $article->setTitle('1'); @@ -62,12 +73,9 @@ public function modifyingANodeWhileItsTreeIsLockedShouldThrowAnException() $this->dm->flush(); } - /** - * @test - */ - public function modifyingANodeWhileItsTreeIsNotLockedShouldNotThrowException() + public function testModifyingANodeWhileItsTreeIsNotLockedShouldNotThrowException(): void { - $this->markTestSkipped("the locking test is failing after removal of scheduleExtraUpdate"); + static::markTestSkipped('the locking test is failing after removal of scheduleExtraUpdate'); $article = $this->createArticle(); $article->setTitle('1'); $article2 = $this->createArticle(); @@ -94,18 +102,18 @@ public function modifyingANodeWhileItsTreeIsNotLockedShouldNotThrowException() $this->dm->flush(); // But this should throw it, because the root of its tree ($article) is still locked - $this->setExpectedException('Gedmo\Exception\TreeLockingException'); + $this->expectException(TreeLockingException::class); - $repo = $this->dm->getRepository(self::ARTICLE); - $article2 = $repo->findOneByTitle('2'); + $repo = $this->dm->getRepository(Article::class); + $article2 = $repo->findOneBy(['title' => '2']); $article2->setTitle('New title 2'); $this->dm->flush(); } - public function createArticle() + public function createArticle(): Article { - $class = self::ARTICLE; + $class = Article::class; return new $class(); } diff --git a/tests/Gedmo/Tree/MaterializedPathORMFeaturesTest.php b/tests/Gedmo/Tree/MaterializedPathORMFeaturesTest.php index 5a4e822bb4..92c4a2df01 100644 --- a/tests/Gedmo/Tree/MaterializedPathORMFeaturesTest.php +++ b/tests/Gedmo/Tree/MaterializedPathORMFeaturesTest.php @@ -1,26 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\MPFeaturesCategory; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MaterializedPathORMFeaturesTest extends BaseTestCaseORM +final class MaterializedPathORMFeaturesTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\MPFeaturesCategory"; - + /** + * @var array + */ protected $config; + + /** + * @var TreeListener + */ protected $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -29,16 +43,13 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($this->listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); - $meta = $this->em->getClassMetadata(self::CATEGORY); - $this->config = $this->listener->getConfiguration($this->em, $meta->name); + $meta = $this->em->getClassMetadata(MPFeaturesCategory::class); + $this->config = $this->listener->getConfiguration($this->em, $meta->getName()); } - /** - * @test - */ - public function checkPathsAndHash() + public function testCheckPathsAndHash(): void { $category = $this->createCategory(); $category->setTitle('1'); @@ -63,32 +74,40 @@ public function checkPathsAndHash() $this->em->refresh($category3); $this->em->refresh($category4); - $this->assertEquals($this->generatePath(array('1' => $category->getId())), $category->getPath()); - $this->assertEquals($this->generatePath(array('1' => $category->getId(), '2' => $category2->getId())), $category2->getPath()); - $this->assertEquals($this->generatePath(array('1' => $category->getId(), '2' => $category2->getId(), '3' => $category3->getId())), $category3->getPath()); - $this->assertEquals($this->generatePath(array('4' => $category4->getId())), $category4->getPath()); + static::assertSame($this->generatePath(['1' => $category->getId()]), $category->getPath()); + static::assertSame($this->generatePath(['1' => $category->getId(), '2' => $category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath(['1' => $category->getId(), '2' => $category2->getId(), '3' => $category3->getId()]), $category3->getPath()); + static::assertSame($this->generatePath(['4' => $category4->getId()]), $category4->getPath()); + + static::assertSame($this->generatePathHash(['1' => $category->getId()]), $category->getPathHash()); + static::assertSame($this->generatePathHash(['1' => $category->getId(), '2' => $category2->getId()]), $category2->getPathHash()); + static::assertSame($this->generatePathHash(['1' => $category->getId(), '2' => $category2->getId(), '3' => $category3->getId()]), $category3->getPathHash()); + static::assertSame($this->generatePathHash(['4' => $category4->getId()]), $category4->getPathHash()); - $this->assertEquals($this->generatePathHash(array('1' => $category->getId())), $category->getPathHash()); - $this->assertEquals($this->generatePathHash(array('1' => $category->getId(), '2' => $category2->getId())), $category2->getPathHash()); - $this->assertEquals($this->generatePathHash(array('1' => $category->getId(), '2' => $category2->getId(), '3' => $category3->getId())), $category3->getPathHash()); - $this->assertEquals($this->generatePathHash(array('4' => $category4->getId())), $category4->getPathHash()); + static::assertSame($category->getTitle(), $category->getTreeRootValue()); + static::assertSame($category->getTitle(), $category2->getTreeRootValue()); + static::assertSame($category->getTitle(), $category3->getTreeRootValue()); + static::assertSame($category4->getTitle(), $category4->getTreeRootValue()); } - public function createCategory() + protected function getUsedEntityFixtures(): array { - $class = self::CATEGORY; - - return new $class(); + return [ + MPFeaturesCategory::class, + ]; } - protected function getUsedEntityFixtures() + private function createCategory(): MPFeaturesCategory { - return array( - self::CATEGORY, - ); + $class = MPFeaturesCategory::class; + + return new $class(); } - public function generatePath(array $sources) + /** + * @param array $sources + */ + private function generatePath(array $sources): string { $path = ''; foreach ($sources as $p => $id) { @@ -98,7 +117,10 @@ public function generatePath(array $sources) return $path; } - public function generatePathHash(array $sources) + /** + * @param array $sources + */ + private function generatePathHash(array $sources): string { return md5($this->generatePath($sources)); } diff --git a/tests/Gedmo/Tree/MaterializedPathORMRepositoryTest.php b/tests/Gedmo/Tree/MaterializedPathORMRepositoryTest.php index 776c87f0cf..50b5cc0f00 100644 --- a/tests/Gedmo/Tree/MaterializedPathORMRepositoryTest.php +++ b/tests/Gedmo/Tree/MaterializedPathORMRepositoryTest.php @@ -1,27 +1,38 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\MPCategory; +use Gedmo\Tests\Tree\Fixture\MPCategoryWithTrimmedSeparator; +use Gedmo\Tree\Entity\Repository\MaterializedPathRepository; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MaterializedPathORMRepositoryTest extends BaseTestCaseORM +final class MaterializedPathORMRepositoryTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\MPCategory"; - const CATEGORY_WITH_TRIMMED_SEPARATOR = "Tree\\Fixture\\MPCategoryWithTrimmedSeparator"; + /** @var MaterializedPathRepository */ + private MaterializedPathRepository $repo; - /** @var $this->repo \Gedmo\Tree\Entity\Repository\MaterializedPathRepository */ - protected $repo; + private TreeListener $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -30,317 +41,297 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($this->listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); - $meta = $this->em->getClassMetadata(self::CATEGORY); - $this->config = $this->listener->getConfiguration($this->em, $meta->name); + $meta = $this->em->getClassMetadata(MPCategory::class); + $this->listener->getConfiguration($this->em, $meta->getName()); $this->populate(); - $this->repo = $this->em->getRepository(self::CATEGORY); + $this->repo = $this->em->getRepository(MPCategory::class); } - /** - * @test - */ - public function getRootNodes() + public function testGetRootNodes(): void { $result = $this->repo->getRootNodes('title'); - $this->assertCount(3, $result); - $this->assertEquals('Drinks', $result[0]->getTitle()); - $this->assertEquals('Food', $result[1]->getTitle()); - $this->assertEquals('Sports', $result[2]->getTitle()); + static::assertCount(3, $result); + static::assertSame('Drinks', $result[0]->getTitle()); + static::assertSame('Food', $result[1]->getTitle()); + static::assertSame('Sports', $result[2]->getTitle()); } - /** - * @test - */ - public function getPath() + public function testGetPath(): void { - $childNode = $this->repo->findOneByTitle('Carrots'); + $childNode = $this->repo->findOneBy(['title' => 'Carrots']); $result = $this->repo->getPath($childNode); - $this->assertCount(3, $result); - $this->assertEquals('Food', $result[0]->getTitle()); - $this->assertEquals('Vegitables', $result[1]->getTitle()); - $this->assertEquals('Carrots', $result[2]->getTitle()); + static::assertCount(3, $result); + static::assertSame('Food', $result[0]->getTitle()); + static::assertSame('Vegitables', $result[1]->getTitle()); + static::assertSame('Carrots', $result[2]->getTitle()); - $rootNode = $this->repo->findOneByTitle('Sports'); + $rootNode = $this->repo->findOneBy(['title' => 'Sports']); $result = $this->repo->getPath($rootNode); - $this->assertCount(1, $result); - $this->assertEquals('Sports', $result[0]->getTitle()); + static::assertCount(1, $result); + static::assertSame('Sports', $result[0]->getTitle()); } - /** - * @test - */ - public function getChildren() + public function testGetChildren(): void { - $root = $this->repo->findOneByTitle('Food'); + $root = $this->repo->findOneBy(['title' => 'Food']); // Get all children from the root, NOT including it $result = $this->repo->getChildren($root, false, 'title'); - $this->assertCount(4, $result); - $this->assertEquals('Carrots', $result[0]->getTitle()); - $this->assertEquals('Fruits', $result[1]->getTitle()); - $this->assertEquals('Potatoes', $result[2]->getTitle()); - $this->assertEquals('Vegitables', $result[3]->getTitle()); + static::assertCount(4, $result); + static::assertSame('Carrots', $result[0]->getTitle()); + static::assertSame('Fruits', $result[1]->getTitle()); + static::assertSame('Potatoes', $result[2]->getTitle()); + static::assertSame('Vegitables', $result[3]->getTitle()); // Get all children from the root, including it $result = $this->repo->getChildren($root, false, 'title', 'asc', true); - $this->assertCount(5, $result); - $this->assertEquals('Carrots', $result[0]->getTitle()); - $this->assertEquals('Food', $result[1]->getTitle()); - $this->assertEquals('Fruits', $result[2]->getTitle()); - $this->assertEquals('Potatoes', $result[3]->getTitle()); - $this->assertEquals('Vegitables', $result[4]->getTitle()); + static::assertCount(5, $result); + static::assertSame('Carrots', $result[0]->getTitle()); + static::assertSame('Food', $result[1]->getTitle()); + static::assertSame('Fruits', $result[2]->getTitle()); + static::assertSame('Potatoes', $result[3]->getTitle()); + static::assertSame('Vegitables', $result[4]->getTitle()); // Get direct children from the root, NOT including it $result = $this->repo->getChildren($root, true, 'title', 'asc'); - $this->assertCount(2, $result); - $this->assertEquals('Fruits', $result[0]->getTitle()); - $this->assertEquals('Vegitables', $result[1]->getTitle()); + static::assertCount(2, $result); + static::assertSame('Fruits', $result[0]->getTitle()); + static::assertSame('Vegitables', $result[1]->getTitle()); // Get direct children from the root, including it $result = $this->repo->getChildren($root, true, 'title', 'asc', true); - $this->assertCount(3, $result); - $this->assertEquals('Food', $result[0]->getTitle()); - $this->assertEquals('Fruits', $result[1]->getTitle()); - $this->assertEquals('Vegitables', $result[2]->getTitle()); + static::assertCount(3, $result); + static::assertSame('Food', $result[0]->getTitle()); + static::assertSame('Fruits', $result[1]->getTitle()); + static::assertSame('Vegitables', $result[2]->getTitle()); // Get ALL nodes $result = $this->repo->getChildren(null, false, 'title'); - $this->assertCount(9, $result); - $this->assertEquals('Best Whisky', $result[0]->getTitle()); - $this->assertEquals('Carrots', $result[1]->getTitle()); - $this->assertEquals('Drinks', $result[2]->getTitle()); - $this->assertEquals('Food', $result[3]->getTitle()); - $this->assertEquals('Fruits', $result[4]->getTitle()); - $this->assertEquals('Potatoes', $result[5]->getTitle()); - $this->assertEquals('Sports', $result[6]->getTitle()); - $this->assertEquals('Vegitables', $result[7]->getTitle()); - $this->assertEquals('Whisky', $result[8]->getTitle()); + static::assertCount(9, $result); + static::assertSame('Best Whisky', $result[0]->getTitle()); + static::assertSame('Carrots', $result[1]->getTitle()); + static::assertSame('Drinks', $result[2]->getTitle()); + static::assertSame('Food', $result[3]->getTitle()); + static::assertSame('Fruits', $result[4]->getTitle()); + static::assertSame('Potatoes', $result[5]->getTitle()); + static::assertSame('Sports', $result[6]->getTitle()); + static::assertSame('Vegitables', $result[7]->getTitle()); + static::assertSame('Whisky', $result[8]->getTitle()); // Get ALL root nodes $result = $this->repo->getChildren(null, true, 'title'); - $this->assertCount(3, $result); - $this->assertEquals('Drinks', $result[0]->getTitle()); - $this->assertEquals('Food', $result[1]->getTitle()); - $this->assertEquals('Sports', $result[2]->getTitle()); + static::assertCount(3, $result); + static::assertSame('Drinks', $result[0]->getTitle()); + static::assertSame('Food', $result[1]->getTitle()); + static::assertSame('Sports', $result[2]->getTitle()); } - /** - * @test - */ - public function getChildrenForEntityWithTrimmedSeparators() + public function testGetChildrenForEntityWithTrimmedSeparators(): void { - $meta = $this->em->getClassMetadata(self::CATEGORY_WITH_TRIMMED_SEPARATOR); - $this->populate(self::CATEGORY_WITH_TRIMMED_SEPARATOR); + $this->populate(MPCategoryWithTrimmedSeparator::class); - $this->repo = $this->em->getRepository(self::CATEGORY_WITH_TRIMMED_SEPARATOR); - $root = $this->repo->findOneByTitle('Food'); + $repo = $this->em->getRepository(MPCategoryWithTrimmedSeparator::class); + $root = $repo->findOneBy(['title' => 'Food']); // Get all children from the root, NOT including it - $result = $this->repo->getChildren($root, false, 'title'); + $result = $repo->getChildren($root, false, 'title'); - $this->assertCount(4, $result); - $this->assertEquals('Carrots', $result[0]->getTitle()); - $this->assertEquals('Fruits', $result[1]->getTitle()); - $this->assertEquals('Potatoes', $result[2]->getTitle()); - $this->assertEquals('Vegitables', $result[3]->getTitle()); + static::assertCount(4, $result); + static::assertSame('Carrots', $result[0]->getTitle()); + static::assertSame('Fruits', $result[1]->getTitle()); + static::assertSame('Potatoes', $result[2]->getTitle()); + static::assertSame('Vegitables', $result[3]->getTitle()); // Get all children from the root, including it - $result = $this->repo->getChildren($root, false, 'title', 'asc', true); + $result = $repo->getChildren($root, false, 'title', 'asc', true); - $this->assertCount(5, $result); - $this->assertEquals('Carrots', $result[0]->getTitle()); - $this->assertEquals('Food', $result[1]->getTitle()); - $this->assertEquals('Fruits', $result[2]->getTitle()); - $this->assertEquals('Potatoes', $result[3]->getTitle()); - $this->assertEquals('Vegitables', $result[4]->getTitle()); + static::assertCount(5, $result); + static::assertSame('Carrots', $result[0]->getTitle()); + static::assertSame('Food', $result[1]->getTitle()); + static::assertSame('Fruits', $result[2]->getTitle()); + static::assertSame('Potatoes', $result[3]->getTitle()); + static::assertSame('Vegitables', $result[4]->getTitle()); // Get direct children from the root, NOT including it - $result = $this->repo->getChildren($root, true, 'title', 'asc'); - $this->assertCount(2, $result); - $this->assertEquals('Fruits', $result[0]->getTitle()); - $this->assertEquals('Vegitables', $result[1]->getTitle()); + $result = $repo->getChildren($root, true, 'title', 'asc'); + static::assertCount(2, $result); + static::assertSame('Fruits', $result[0]->getTitle()); + static::assertSame('Vegitables', $result[1]->getTitle()); // Get direct children from the root, including it - $result = $this->repo->getChildren($root, true, 'title', 'asc', true); + $result = $repo->getChildren($root, true, 'title', 'asc', true); - $this->assertCount(3, $result); - $this->assertEquals('Food', $result[0]->getTitle()); - $this->assertEquals('Fruits', $result[1]->getTitle()); - $this->assertEquals('Vegitables', $result[2]->getTitle()); + static::assertCount(3, $result); + static::assertSame('Food', $result[0]->getTitle()); + static::assertSame('Fruits', $result[1]->getTitle()); + static::assertSame('Vegitables', $result[2]->getTitle()); // Get ALL nodes - $result = $this->repo->getChildren(null, false, 'title'); - - $this->assertCount(9, $result); - $this->assertEquals('Best Whisky', $result[0]->getTitle()); - $this->assertEquals('Carrots', $result[1]->getTitle()); - $this->assertEquals('Drinks', $result[2]->getTitle()); - $this->assertEquals('Food', $result[3]->getTitle()); - $this->assertEquals('Fruits', $result[4]->getTitle()); - $this->assertEquals('Potatoes', $result[5]->getTitle()); - $this->assertEquals('Sports', $result[6]->getTitle()); - $this->assertEquals('Vegitables', $result[7]->getTitle()); - $this->assertEquals('Whisky', $result[8]->getTitle()); + $result = $repo->getChildren(null, false, 'title'); + + static::assertCount(9, $result); + static::assertSame('Best Whisky', $result[0]->getTitle()); + static::assertSame('Carrots', $result[1]->getTitle()); + static::assertSame('Drinks', $result[2]->getTitle()); + static::assertSame('Food', $result[3]->getTitle()); + static::assertSame('Fruits', $result[4]->getTitle()); + static::assertSame('Potatoes', $result[5]->getTitle()); + static::assertSame('Sports', $result[6]->getTitle()); + static::assertSame('Vegitables', $result[7]->getTitle()); + static::assertSame('Whisky', $result[8]->getTitle()); // Get ALL root nodes - $result = $this->repo->getChildren(null, true, 'title'); + $result = $repo->getChildren(null, true, 'title'); - $this->assertCount(3, $result); - $this->assertEquals('Drinks', $result[0]->getTitle()); - $this->assertEquals('Food', $result[1]->getTitle()); - $this->assertEquals('Sports', $result[2]->getTitle()); + static::assertCount(3, $result); + static::assertSame('Drinks', $result[0]->getTitle()); + static::assertSame('Food', $result[1]->getTitle()); + static::assertSame('Sports', $result[2]->getTitle()); } - /** - * @test - */ - public function getTree() + public function testGetTree(): void { $tree = $this->repo->getTree(); - $this->assertCount(9, $tree); - $this->assertEquals('Drinks', $tree[0]->getTitle()); - $this->assertEquals('Whisky', $tree[1]->getTitle()); - $this->assertEquals('Best Whisky', $tree[2]->getTitle()); - $this->assertEquals('Food', $tree[3]->getTitle()); - $this->assertEquals('Fruits', $tree[4]->getTitle()); - $this->assertEquals('Vegitables', $tree[5]->getTitle()); - $this->assertEquals('Carrots', $tree[6]->getTitle()); - $this->assertEquals('Potatoes', $tree[7]->getTitle()); - $this->assertEquals('Sports', $tree[8]->getTitle()); + static::assertCount(9, $tree); + static::assertSame('Drinks', $tree[0]->getTitle()); + static::assertSame('Whisky', $tree[1]->getTitle()); + static::assertSame('Best Whisky', $tree[2]->getTitle()); + static::assertSame('Food', $tree[3]->getTitle()); + static::assertSame('Fruits', $tree[4]->getTitle()); + static::assertSame('Vegitables', $tree[5]->getTitle()); + static::assertSame('Carrots', $tree[6]->getTitle()); + static::assertSame('Potatoes', $tree[7]->getTitle()); + static::assertSame('Sports', $tree[8]->getTitle()); // Get tree from a specific root node $roots = $this->repo->getRootNodes(); $tree = $this->repo->getTree($roots[0]); - $this->assertCount(3, $tree); - $this->assertEquals('Drinks', $tree[0]->getTitle()); - $this->assertEquals('Whisky', $tree[1]->getTitle()); - $this->assertEquals('Best Whisky', $tree[2]->getTitle()); + static::assertCount(3, $tree); + static::assertSame('Drinks', $tree[0]->getTitle()); + static::assertSame('Whisky', $tree[1]->getTitle()); + static::assertSame('Best Whisky', $tree[2]->getTitle()); } - public function testChildrenHierarchyMethod() + public function testChildrenHierarchyMethod(): void { $tree = $this->repo->childrenHierarchy(); - $this->assertEquals('Drinks', $tree[0]['title']); - $this->assertEquals('Whisky', $tree[0]['__children'][0]['title']); - $this->assertEquals('Best Whisky', $tree[0]['__children'][0]['__children'][0]['title']); + static::assertSame('Drinks', $tree[0]['title']); + static::assertSame('Whisky', $tree[0]['__children'][0]['title']); + static::assertSame('Best Whisky', $tree[0]['__children'][0]['__children'][0]['title']); $vegitablesChildren = $tree[1]['__children'][1]['__children']; - $this->assertEquals('Food', $tree[1]['title']); - $this->assertEquals('Fruits', $tree[1]['__children'][0]['title']); - $this->assertEquals('Vegitables', $tree[1]['__children'][1]['title']); - $this->assertEquals('Carrots', $vegitablesChildren[0]['title']); - $this->assertEquals('Potatoes', $vegitablesChildren[1]['title']); - $this->assertEquals('Sports', $tree[2]['title']); + static::assertSame('Food', $tree[1]['title']); + static::assertSame('Fruits', $tree[1]['__children'][0]['title']); + static::assertSame('Vegitables', $tree[1]['__children'][1]['title']); + static::assertSame('Carrots', $vegitablesChildren[0]['title']); + static::assertSame('Potatoes', $vegitablesChildren[1]['title']); + static::assertSame('Sports', $tree[2]['title']); // Tree of one specific root, without the root node $roots = $this->repo->getRootNodes(); $tree = $this->repo->childrenHierarchy($roots[0]); - $this->assertEquals('Whisky', $tree[0]['title']); - $this->assertEquals('Best Whisky', $tree[0]['__children'][0]['title']); + static::assertSame('Whisky', $tree[0]['title']); + static::assertSame('Best Whisky', $tree[0]['__children'][0]['title']); // Tree of one specific root, with the root node - $tree = $this->repo->childrenHierarchy($roots[0], false, array(), true); + $tree = $this->repo->childrenHierarchy($roots[0], false, [], true); - $this->assertEquals('Drinks', $tree[0]['title']); - $this->assertEquals('Whisky', $tree[0]['__children'][0]['title']); - $this->assertEquals('Best Whisky', $tree[0]['__children'][0]['__children'][0]['title']); + static::assertSame('Drinks', $tree[0]['title']); + static::assertSame('Whisky', $tree[0]['__children'][0]['title']); + static::assertSame('Best Whisky', $tree[0]['__children'][0]['__children'][0]['title']); // Tree of one specific root only with direct children, without the root node $roots = $this->repo->getRootNodes(); $tree = $this->repo->childrenHierarchy($roots[1], true); - $this->assertEquals(2, count($tree)); - $this->assertEquals('Fruits', $tree[0]['title']); - $this->assertEquals('Vegitables', $tree[1]['title']); + static::assertCount(2, $tree); + static::assertSame('Fruits', $tree[0]['title']); + static::assertSame('Vegitables', $tree[1]['title']); // Tree of one specific root only with direct children, with the root node - $tree = $this->repo->childrenHierarchy($roots[1], true, array(), true); + $tree = $this->repo->childrenHierarchy($roots[1], true, [], true); - $this->assertEquals(1, count($tree)); - $this->assertEquals(2, count($tree[0]['__children'])); - $this->assertEquals('Food', $tree[0]['title']); - $this->assertEquals('Fruits', $tree[0]['__children'][0]['title']); - $this->assertEquals('Vegitables', $tree[0]['__children'][1]['title']); + static::assertCount(1, $tree); + static::assertCount(2, $tree[0]['__children']); + static::assertSame('Food', $tree[0]['title']); + static::assertSame('Fruits', $tree[0]['__children'][0]['title']); + static::assertSame('Vegitables', $tree[0]['__children'][1]['title']); // HTML Tree of one specific root, without the root node $roots = $this->repo->getRootNodes(); - $tree = $this->repo->childrenHierarchy($roots[0], false, array('decorate' => true), false); + $tree = $this->repo->childrenHierarchy($roots[0], false, ['decorate' => true], false); - $this->assertEquals('
            • Whisky
              • Best Whisky
            ', $tree); + static::assertSame('
            • Whisky
              • Best Whisky
            ', $tree); // HTML Tree of one specific root, with the root node $roots = $this->repo->getRootNodes(); - $tree = $this->repo->childrenHierarchy($roots[0], false, array('decorate' => true), true); + $tree = $this->repo->childrenHierarchy($roots[0], false, ['decorate' => true], true); - $this->assertEquals('
            • Drinks
              • Whisky
                • Best Whisky
            ', $tree); + static::assertSame('
            • Drinks
              • Whisky
                • Best Whisky
            ', $tree); } - public function testChildCount() + public function testChildCount(): void { // Count all $count = $this->repo->childCount(); - $this->assertEquals(9, $count); + static::assertSame(9, $count); // Count all, but only direct ones $count = $this->repo->childCount(null, true); - $this->assertEquals(3, $count); + static::assertSame(3, $count); // Count food children - $food = $this->repo->findOneByTitle('Food'); + $food = $this->repo->findOneBy(['title' => 'Food']); $count = $this->repo->childCount($food); - $this->assertEquals(4, $count); + static::assertSame(4, $count); // Count food children, but only direct ones $count = $this->repo->childCount($food, true); - $this->assertEquals(2, $count); + static::assertSame(2, $count); } - /** - * @expectedException \Gedmo\Exception\InvalidArgumentException - */ - public function testChildCount_ifAnObjectIsPassedWhichIsNotAnInstanceOfTheEntityClassThrowException() + public function testChildCountIfAnObjectIsPassedWhichIsNotAnInstanceOfTheEntityClassThrowException(): void { + $this->expectException(InvalidArgumentException::class); $this->repo->childCount(new \DateTime()); } - /** - * @expectedException \Gedmo\Exception\InvalidArgumentException - */ - public function testChildCount_ifAnObjectIsPassedIsAnInstanceOfTheEntityClassButIsNotHandledByUnitOfWorkThrowException() + public function testChildCountIfAnObjectIsPassedIsAnInstanceOfTheEntityClassButIsNotHandledByUnitOfWorkThrowException(): void { + $this->expectException(InvalidArgumentException::class); $this->repo->childCount($this->createCategory()); } - public function test_issue458() + public function testIssue458(): void { $this->em->clear(); - $node = $this->repo->findOneByTitle('Fruits'); + $node = $this->repo->findOneBy(['title' => 'Fruits']); $newNode = $this->createCategory(); $parent = $node->getParent(); - $this->assertFalse($parent->__isInitialized()); + static::assertTrue($this->em->isUninitializedObject($parent)); $newNode->setTitle('New Node'); $newNode->setParent($parent); @@ -348,59 +339,66 @@ public function test_issue458() $this->em->persist($newNode); $this->em->flush(); - $this->assertRegexp('/Food\-\d+,New\sNode\-\d+/', $newNode->getPath()); - $this->assertEquals(2, $newNode->getLevel()); + static::assertMatchesRegularExpression('/Food\-\d+,New\sNode\-\d+/', $newNode->getPath()); + + static::assertSame(2, $newNode->getLevel()); } - public function test_changeChildrenIndex() + public function testChangeChildrenIndex(): void { $childrenIndex = 'myChildren'; $this->repo->setChildrenIndex($childrenIndex); $tree = $this->repo->childrenHierarchy(); - $this->assertInternalType('array', $tree[0][$childrenIndex]); + static::assertIsArray($tree[0][$childrenIndex]); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CATEGORY, - self::CATEGORY_WITH_TRIMMED_SEPARATOR, - ); + return [ + MPCategory::class, + MPCategoryWithTrimmedSeparator::class, + ]; } - public function createCategory($class = null) + /** + * @phpstan-param class-string|null $class + */ + private function createCategory(?string $class = null): object { if (!$class) { - $class = self::CATEGORY; + $class = MPCategory::class; } return new $class(); } - private function populate($class = null) + /** + * @phpstan-param class-string|null $class + */ + private function populate(?string $class = null): void { $root = $this->createCategory($class); - $root->setTitle("Food"); + $root->setTitle('Food'); $root2 = $this->createCategory($class); - $root2->setTitle("Sports"); + $root2->setTitle('Sports'); $child = $this->createCategory($class); - $child->setTitle("Fruits"); + $child->setTitle('Fruits'); $child->setParent($root); $child2 = $this->createCategory($class); - $child2->setTitle("Vegitables"); + $child2->setTitle('Vegitables'); $child2->setParent($root); $childsChild = $this->createCategory($class); - $childsChild->setTitle("Carrots"); + $childsChild->setTitle('Carrots'); $childsChild->setParent($child2); $potatoes = $this->createCategory($class); - $potatoes->setTitle("Potatoes"); + $potatoes->setTitle('Potatoes'); $potatoes->setParent($child2); $drinks = $this->createCategory($class); diff --git a/tests/Gedmo/Tree/MaterializedPathORMRootAssociationTest.php b/tests/Gedmo/Tree/MaterializedPathORMRootAssociationTest.php new file mode 100644 index 0000000000..a1067b16b3 --- /dev/null +++ b/tests/Gedmo/Tree/MaterializedPathORMRootAssociationTest.php @@ -0,0 +1,157 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\MPCategoryWithRootAssociation; +use Gedmo\Tree\TreeListener; + +/** + * These are tests for Tree behavior + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + */ +final class MaterializedPathORMRootAssociationTest extends BaseTestCaseORM +{ + /** + * @var array + */ + protected $config; + + /** + * @var TreeListener + */ + protected $listener; + + protected function setUp(): void + { + parent::setUp(); + + $this->listener = new TreeListener(); + + $evm = new EventManager(); + $evm->addEventSubscriber($this->listener); + + $this->getDefaultMockSqliteEntityManager($evm); + + $meta = $this->em->getClassMetadata(MPCategoryWithRootAssociation::class); + $this->config = $this->listener->getConfiguration($this->em, $meta->getName()); + } + + public function testInsertUpdateAndRemove(): void + { + // Insert + $category = $this->createCategory(); + $category->setTitle('1'); + $category2 = $this->createCategory(); + $category2->setTitle('2'); + $category3 = $this->createCategory(); + $category3->setTitle('3'); + $category4 = $this->createCategory(); + $category4->setTitle('4'); + + $category2->setParent($category); + $category3->setParent($category2); + + $this->em->persist($category4); + $this->em->persist($category3); + $this->em->persist($category2); + $this->em->persist($category); + $this->em->flush(); + + $this->em->refresh($category); + $this->em->refresh($category2); + $this->em->refresh($category3); + $this->em->refresh($category4); + + static::assertSame($this->generatePath([$category->getId()]), $category->getPath()); + static::assertSame($this->generatePath([$category->getId(), $category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath([$category->getId(), $category2->getId(), $category3->getId()]), $category3->getPath()); + static::assertSame($this->generatePath([$category4->getId()]), $category4->getPath()); + static::assertSame(1, $category->getLevel()); + static::assertSame(2, $category2->getLevel()); + static::assertSame(3, $category3->getLevel()); + static::assertSame(1, $category4->getLevel()); + + static::assertSame($category, $category->getTreeRootEntity()); + static::assertSame($category, $category2->getTreeRootEntity()); + static::assertSame($category, $category3->getTreeRootEntity()); + static::assertSame($category4, $category4->getTreeRootEntity()); + + // Update + $category2->setParent(null); + + $this->em->persist($category2); + $this->em->flush(); + + $this->em->refresh($category); + $this->em->refresh($category2); + $this->em->refresh($category3); + + static::assertSame($this->generatePath([$category->getId()]), $category->getPath()); + static::assertSame($this->generatePath([$category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath([$category2->getId(), $category3->getId()]), $category3->getPath()); + static::assertSame(1, $category->getLevel()); + static::assertSame(1, $category2->getLevel()); + static::assertSame(2, $category3->getLevel()); + static::assertSame(1, $category4->getLevel()); + + static::assertSame($category, $category->getTreeRootEntity()); + static::assertSame($category2, $category2->getTreeRootEntity()); + static::assertSame($category2, $category3->getTreeRootEntity()); + static::assertSame($category4, $category4->getTreeRootEntity()); + + // Remove + $this->em->remove($category); + $this->em->remove($category2); + $this->em->flush(); + + $result = $this->em->createQueryBuilder()->select('c')->from(MPCategoryWithRootAssociation::class, 'c')->getQuery()->getResult(); + + $firstResult = $result[0]; + + static::assertCount(1, $result); + static::assertSame('4', $firstResult->getTitle()); + static::assertSame(1, $firstResult->getLevel()); + static::assertSame($category4, $firstResult->getTreeRootEntity()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + MPCategoryWithRootAssociation::class, + ]; + } + + private function createCategory(): MPCategoryWithRootAssociation + { + $class = MPCategoryWithRootAssociation::class; + + return new $class(); + } + + /** + * @param array $sources + */ + private function generatePath(array $sources): string + { + $path = ''; + + foreach ($sources as $id) { + $path .= $id.$this->config['path_separator']; + } + + return $path; + } +} diff --git a/tests/Gedmo/Tree/MaterializedPathORMTest.php b/tests/Gedmo/Tree/MaterializedPathORMTest.php index eb01004fd4..e06905d3f3 100644 --- a/tests/Gedmo/Tree/MaterializedPathORMTest.php +++ b/tests/Gedmo/Tree/MaterializedPathORMTest.php @@ -1,26 +1,41 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; +use Gedmo\Exception\RuntimeException; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\MPCategory; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MaterializedPathORMTest extends BaseTestCaseORM +final class MaterializedPathORMTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\MPCategory"; - + /** + * @var array + */ protected $config; + + /** + * @var TreeListener + */ protected $listener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -29,16 +44,13 @@ protected function setUp() $evm = new EventManager(); $evm->addEventSubscriber($this->listener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); - $meta = $this->em->getClassMetadata(self::CATEGORY); - $this->config = $this->listener->getConfiguration($this->em, $meta->name); + $meta = $this->em->getClassMetadata(MPCategory::class); + $this->config = $this->listener->getConfiguration($this->em, $meta->getName()); } - /** - * @test - */ - public function insertUpdateAndRemove() + public function testInsertUpdateAndRemove(): void { // Insert $category = $this->createCategory(); @@ -64,14 +76,19 @@ public function insertUpdateAndRemove() $this->em->refresh($category3); $this->em->refresh($category4); - $this->assertEquals($this->generatePath(array('1' => $category->getId())), $category->getPath()); - $this->assertEquals($this->generatePath(array('1' => $category->getId(), '2' => $category2->getId())), $category2->getPath()); - $this->assertEquals($this->generatePath(array('1' => $category->getId(), '2' => $category2->getId(), '3' => $category3->getId())), $category3->getPath()); - $this->assertEquals($this->generatePath(array('4' => $category4->getId())), $category4->getPath()); - $this->assertEquals(1, $category->getLevel()); - $this->assertEquals(2, $category2->getLevel()); - $this->assertEquals(3, $category3->getLevel()); - $this->assertEquals(1, $category4->getLevel()); + static::assertSame($this->generatePath(['1' => $category->getId()]), $category->getPath()); + static::assertSame($this->generatePath(['1' => $category->getId(), '2' => $category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath(['1' => $category->getId(), '2' => $category2->getId(), '3' => $category3->getId()]), $category3->getPath()); + static::assertSame($this->generatePath(['4' => $category4->getId()]), $category4->getPath()); + static::assertSame(1, $category->getLevel()); + static::assertSame(2, $category2->getLevel()); + static::assertSame(3, $category3->getLevel()); + static::assertSame(1, $category4->getLevel()); + + static::assertSame($this->getTreeRootValueOfRootNode($category), $category->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category2), $category2->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category3), $category3->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category4), $category4->getTreeRootValue()); // Update $category2->setParent(null); @@ -83,34 +100,37 @@ public function insertUpdateAndRemove() $this->em->refresh($category2); $this->em->refresh($category3); - $this->assertEquals($this->generatePath(array('1' => $category->getId())), $category->getPath()); - $this->assertEquals($this->generatePath(array('2' => $category2->getId())), $category2->getPath()); - $this->assertEquals($this->generatePath(array('2' => $category2->getId(), '3' => $category3->getId())), $category3->getPath()); - $this->assertEquals(1, $category->getLevel()); - $this->assertEquals(1, $category2->getLevel()); - $this->assertEquals(2, $category3->getLevel()); - $this->assertEquals(1, $category4->getLevel()); + static::assertSame($this->generatePath(['1' => $category->getId()]), $category->getPath()); + static::assertSame($this->generatePath(['2' => $category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath(['2' => $category2->getId(), '3' => $category3->getId()]), $category3->getPath()); + static::assertSame(1, $category->getLevel()); + static::assertSame(1, $category2->getLevel()); + static::assertSame(2, $category3->getLevel()); + static::assertSame(1, $category4->getLevel()); + + static::assertSame($this->getTreeRootValueOfRootNode($category), $category->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category2), $category2->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category3), $category3->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category4), $category4->getTreeRootValue()); // Remove $this->em->remove($category); $this->em->remove($category2); $this->em->flush(); - $result = $this->em->createQueryBuilder()->select('c')->from(self::CATEGORY, 'c')->getQuery()->execute(); + $result = $this->em->createQueryBuilder()->select('c')->from(MPCategory::class, 'c')->getQuery()->getResult(); $firstResult = $result[0]; - $this->assertCount(1, $result); - $this->assertEquals('4', $firstResult->getTitle()); - $this->assertEquals(1, $firstResult->getLevel()); + static::assertCount(1, $result); + static::assertSame('4', $firstResult->getTitle()); + static::assertSame(1, $firstResult->getLevel()); + static::assertSame($this->getTreeRootValueOfRootNode($firstResult), $firstResult->getTreeRootValue()); } - /** - * @test - */ - public function useOfSeparatorInPathSourceShouldThrowAnException() + public function testUseOfSeparatorInPathSourceShouldThrowAnException(): void { - $this->setExpectedException('Gedmo\Exception\RuntimeException'); + $this->expectException(RuntimeException::class); $category = $this->createCategory(); $category->setTitle('1'.$this->config['path_separator']); @@ -119,21 +139,24 @@ public function useOfSeparatorInPathSourceShouldThrowAnException() $this->em->flush(); } - public function createCategory() + protected function getUsedEntityFixtures(): array { - $class = self::CATEGORY; - - return new $class(); + return [ + MPCategory::class, + ]; } - protected function getUsedEntityFixtures() + private function createCategory(): MPCategory { - return array( - self::CATEGORY, - ); + $class = MPCategory::class; + + return new $class(); } - public function generatePath(array $sources) + /** + * @param array $sources + */ + private function generatePath(array $sources): string { $path = ''; @@ -143,4 +166,13 @@ public function generatePath(array $sources) return $path; } + + private function getTreeRootValueOfRootNode(MPCategory $category): string + { + while (null !== $category->getParent()) { + $category = $category->getParent(); + } + + return $category->getTreeRootValue(); + } } diff --git a/tests/Gedmo/Tree/MaterializedPathUuidORMTest.php b/tests/Gedmo/Tree/MaterializedPathUuidORMTest.php new file mode 100644 index 0000000000..dc62f4b5d7 --- /dev/null +++ b/tests/Gedmo/Tree/MaterializedPathUuidORMTest.php @@ -0,0 +1,171 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\MPCategoryUuid; +use Gedmo\Tree\TreeListener; +use Symfony\Component\Uid\UuidV4; + +/** + * These are tests for Tree behavior when using Uuid as primary key and TreePathSource. + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * @author Andrea Bergamasco + */ +final class MaterializedPathUuidORMTest extends BaseTestCaseORM +{ + /** + * @var array + */ + protected array $config; + + protected TreeListener $listener; + + protected function setUp(): void + { + parent::setUp(); + + $this->listener = new TreeListener(); + + $evm = new EventManager(); + $evm->addEventSubscriber($this->listener); + + $this->getDefaultMockSqliteEntityManager($evm); + + $meta = $this->em->getClassMetadata(MPCategoryUuid::class); + $this->config = $this->listener->getConfiguration($this->em, $meta->getName()); + } + + public function testInsertUpdateAndRemove(): void + { + // Insert + $category = $this->createCategory(); + $category->setTitle('1'); + static::assertNotNull($category->getId()); + $category2 = $this->createCategory(); + $category2->setTitle('2'); + static::assertNotNull($category2->getId()); + $category3 = $this->createCategory(); + $category3->setTitle('3'); + static::assertNotNull($category3->getId()); + $category4 = $this->createCategory(); + $category4->setTitle('4'); + static::assertNotNull($category4->getId()); + + $category2->setParent($category); + $category3->setParent($category2); + + $this->em->persist($category4); + $this->em->persist($category3); + $this->em->persist($category2); + $this->em->persist($category); + $this->em->flush(); + + $this->em->refresh($category); + $this->em->refresh($category2); + $this->em->refresh($category3); + $this->em->refresh($category4); + + static::assertSame($this->generatePath([$category->getId()]), $category->getPath()); + static::assertSame($this->generatePath([$category->getId(), $category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath([$category->getId(), $category2->getId(), $category3->getId()]), $category3->getPath()); + static::assertSame($this->generatePath([$category4->getId()]), $category4->getPath()); + static::assertSame(1, $category->getLevel()); + static::assertSame(2, $category2->getLevel()); + static::assertSame(3, $category3->getLevel()); + static::assertSame(1, $category4->getLevel()); + + static::assertSame($this->getTreeRootValueOfRootNode($category), $category->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category2), $category2->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category3), $category3->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category4), $category4->getTreeRootValue()); + + // Update + $category2->setParent(null); + + $this->em->persist($category2); + $this->em->flush(); + + $this->em->refresh($category); + $this->em->refresh($category2); + $this->em->refresh($category3); + + static::assertSame($this->generatePath([$category->getId()]), $category->getPath()); + static::assertSame($this->generatePath([$category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath([$category2->getId(), $category3->getId()]), $category3->getPath()); + static::assertSame(1, $category->getLevel()); + static::assertSame(1, $category2->getLevel()); + static::assertSame(2, $category3->getLevel()); + static::assertSame(1, $category4->getLevel()); + + static::assertSame($this->getTreeRootValueOfRootNode($category), $category->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category2), $category2->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category3), $category3->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category4), $category4->getTreeRootValue()); + + // Remove + $this->em->remove($category); + $this->em->remove($category2); + $this->em->flush(); + + $result = $this->em->createQueryBuilder()->select('c')->from(MPCategoryUuid::class, 'c')->getQuery()->getResult(); + + $firstResult = $result[0]; + + static::assertCount(1, $result); + static::assertSame('4', $firstResult->getTitle()); + static::assertSame(1, $firstResult->getLevel()); + static::assertSame($this->getTreeRootValueOfRootNode($firstResult), $firstResult->getTreeRootValue()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + MPCategoryUuid::class, + ]; + } + + private function createCategory(): MPCategoryUuid + { + return new MPCategoryUuid(); + } + + /** + * @param array $sources + */ + private function generatePath(array $sources): string + { + $path = $this->config['path_starts_with_separator'] + ? $this->config['path_separator'] + : ''; + + $path .= implode($this->config['path_separator'], $sources); + + $path .= $this->config['path_ends_with_separator'] + ? $this->config['path_separator'] + : ''; + + return $path; + } + + private function getTreeRootValueOfRootNode(MPCategoryUuid $category): string + { + while (null !== $category->getParent()) { + $category = $category->getParent(); + } + + return $category->getTreeRootValue(); + } +} diff --git a/tests/Gedmo/Tree/MultInheritanceWithJoinedTableTest.php b/tests/Gedmo/Tree/MultInheritanceWithJoinedTableTest.php index a4dfbe28aa..862818b030 100644 --- a/tests/Gedmo/Tree/MultInheritanceWithJoinedTableTest.php +++ b/tests/Gedmo/Tree/MultInheritanceWithJoinedTableTest.php @@ -1,9 +1,23 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Role; +use Gedmo\Tests\Tree\Fixture\User; +use Gedmo\Tests\Tree\Fixture\UserGroup; +use Gedmo\Tests\Tree\Fixture\UserLDAP; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior @@ -11,17 +25,12 @@ * JOINED table inheritance mapping bug on Tree; * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MultInheritanceWithJoinedTableTest extends BaseTestCaseORM +final class MultInheritanceWithJoinedTableTest extends BaseTestCaseORM { - const USER = "Tree\\Fixture\\User"; - const GROUP = "Tree\\Fixture\\UserGroup"; - const ROLE = "Tree\\Fixture\\Role"; - const USERLDAP = "Tree\\Fixture\\UserLDAP"; + private TreeListener $tree; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -29,35 +38,29 @@ protected function setUp() $this->tree = new TreeListener(); $evm->addEventSubscriber($this->tree); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - /** - * @test - */ - public function shouldHandleMultilevelInheritance() + public function testShouldHandleMultilevelInheritance(): void { - $admins = $this->em->getRepository(self::GROUP)->findOneByName('Admins'); + $admins = $this->em->getRepository(UserGroup::class)->findOneBy(['name' => 'Admins']); $adminRight = $admins->getRight(); - $userLdap = new \Tree\Fixture\UserLDAP('testname'); + $userLdap = new UserLDAP('testname'); $userLdap->init(); $userLdap->setParent($admins); $this->em->persist($userLdap); $this->em->flush(); $this->em->clear(); - $admins = $this->em->getRepository(self::GROUP)->findOneByName('Admins'); - self::assertNotEquals($adminRight, $admins->getRight()); + $admins = $this->em->getRepository(UserGroup::class)->findOneBy(['name' => 'Admins']); + static::assertNotSame($adminRight, $admins->getRight()); } - /** - * @test - */ - public function shouldBeAbleToPopulateTree() + public function testShouldBeAbleToPopulateTree(): void { - $admins = $this->em->getRepository(self::GROUP)->findOneByName('Admins'); - $user3 = new \Tree\Fixture\User('user3@test.com', 'secret'); + $admins = $this->em->getRepository(UserGroup::class)->findOneBy(['name' => 'Admins']); + $user3 = new User('user3@test.com', 'secret'); $user3->init(); $user3->setParent($admins); @@ -67,67 +70,67 @@ public function shouldBeAbleToPopulateTree() // run tree consistence checks - $everyBody = $this->em->getRepository(self::GROUP)->findOneByName('Everybody'); - $this->assertEquals(1, $everyBody->getLeft()); - $this->assertEquals(14, $everyBody->getRight()); - $this->assertEquals(0, $everyBody->getLevel()); - - $admins = $this->em->getRepository(self::GROUP)->findOneByName('Admins'); - $this->assertEquals(2, $admins->getLeft()); - $this->assertEquals(7, $admins->getRight()); - $this->assertEquals(1, $admins->getLevel()); - - $visitors = $this->em->getRepository(self::GROUP)->findOneByName('Visitors'); - $this->assertEquals(8, $visitors->getLeft()); - $this->assertEquals(13, $visitors->getRight()); - $this->assertEquals(1, $visitors->getLevel()); - - $user0 = $this->em->getRepository(self::USER)->findOneByEmail('user0@test.com'); - $this->assertEquals(3, $user0->getLeft()); - $this->assertEquals(4, $user0->getRight()); - $this->assertEquals(2, $user0->getLevel()); - - $user1 = $this->em->getRepository(self::USER)->findOneByEmail('user1@test.com'); - $this->assertEquals(9, $user1->getLeft()); - $this->assertEquals(10, $user1->getRight()); - $this->assertEquals(2, $user1->getLevel()); - - $user2 = $this->em->getRepository(self::USER)->findOneByEmail('user2@test.com'); - $this->assertEquals(11, $user2->getLeft()); - $this->assertEquals(12, $user2->getRight()); - $this->assertEquals(2, $user2->getLevel()); - - $user3 = $this->em->getRepository(self::USER)->findOneByEmail('user3@test.com'); - $this->assertEquals(5, $user3->getLeft()); - $this->assertEquals(6, $user3->getRight()); - $this->assertEquals(2, $user3->getLevel()); + $everyBody = $this->em->getRepository(UserGroup::class)->findOneBy(['name' => 'Everybody']); + static::assertSame(1, $everyBody->getLeft()); + static::assertSame(14, $everyBody->getRight()); + static::assertSame(0, $everyBody->getLevel()); + + $admins = $this->em->getRepository(UserGroup::class)->findOneBy(['name' => 'Admins']); + static::assertSame(2, $admins->getLeft()); + static::assertSame(7, $admins->getRight()); + static::assertSame(1, $admins->getLevel()); + + $visitors = $this->em->getRepository(UserGroup::class)->findOneBy(['name' => 'Visitors']); + static::assertSame(8, $visitors->getLeft()); + static::assertSame(13, $visitors->getRight()); + static::assertSame(1, $visitors->getLevel()); + + $user0 = $this->em->getRepository(User::class)->findOneBy(['email' => 'user0@test.com']); + static::assertSame(3, $user0->getLeft()); + static::assertSame(4, $user0->getRight()); + static::assertSame(2, $user0->getLevel()); + + $user1 = $this->em->getRepository(User::class)->findOneBy(['email' => 'user1@test.com']); + static::assertSame(9, $user1->getLeft()); + static::assertSame(10, $user1->getRight()); + static::assertSame(2, $user1->getLevel()); + + $user2 = $this->em->getRepository(User::class)->findOneBy(['email' => 'user2@test.com']); + static::assertSame(11, $user2->getLeft()); + static::assertSame(12, $user2->getRight()); + static::assertSame(2, $user2->getLevel()); + + $user3 = $this->em->getRepository(User::class)->findOneBy(['email' => 'user3@test.com']); + static::assertSame(5, $user3->getLeft()); + static::assertSame(6, $user3->getRight()); + static::assertSame(2, $user3->getLevel()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::USER, - self::GROUP, - self::ROLE, - self::USERLDAP, - ); + return [ + User::class, + UserGroup::class, + Role::class, + UserLDAP::class, + ]; } - private function populate() + private function populate(): void { - $everyBody = new \Tree\Fixture\UserGroup('Everybody'); - $admins = new \Tree\Fixture\UserGroup('Admins'); + $everyBody = new UserGroup('Everybody'); + $admins = new UserGroup('Admins'); $admins->setParent($everyBody); - $visitors = new \Tree\Fixture\UserGroup('Visitors'); + $visitors = new UserGroup('Visitors'); $visitors->setParent($everyBody); - $user0 = new \Tree\Fixture\User('user0@test.com', 'secret'); + $user0 = new User('user0@test.com', 'secret'); $user0->init(); $user0->setParent($admins); - $user1 = new \Tree\Fixture\User('user1@test.com', 'secret'); + $user1 = new User('user1@test.com', 'secret'); $user1->init(); $user1->setParent($visitors); - $user2 = new \Tree\Fixture\User('user2@test.com', 'secret'); + $user2 = new User('user2@test.com', 'secret'); $user2->init(); $user2->setParent($visitors); diff --git a/tests/Gedmo/Tree/MultiInheritanceTest.php b/tests/Gedmo/Tree/MultiInheritanceTest.php index 58cbc0dc81..6ed11052d9 100644 --- a/tests/Gedmo/Tree/MultiInheritanceTest.php +++ b/tests/Gedmo/Tree/MultiInheritanceTest.php @@ -1,49 +1,54 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; + +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\ANode; +use Gedmo\Tests\Tree\Fixture\BaseNode; +use Gedmo\Tests\Tree\Fixture\Node; +use Gedmo\Translatable\Entity\Translation; /** * These are tests for Tree behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MultiInheritanceTest extends BaseTestCaseORM +final class MultiInheritanceTest extends BaseTestCaseORM { - const NODE = "Tree\\Fixture\\Node"; - const BASE_NODE = "Tree\\Fixture\\BaseNode"; - const ANODE = "Tree\\Fixture\\ANode"; - const TRANSLATION = "Gedmo\\Translatable\\Entity\\Translation"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); - $this->getMockSqliteEntityManager(); + $this->getDefaultMockSqliteEntityManager(); $this->populate(); } - public function testInheritance() + public function testInheritance(): void { - $meta = $this->em->getClassMetadata(self::NODE); - $repo = $this->em->getRepository(self::NODE); + $meta = $this->em->getClassMetadata(Node::class); + $repo = $this->em->getRepository(Node::class); - $food = $repo->findOneByIdentifier('food'); + $food = $repo->findOneBy(['identifier' => 'food']); $left = $meta->getReflectionProperty('lft')->getValue($food); - $right = $meta->getReflectionProperty('rgt')->getValue($food); - $this->assertEquals(1, $left); - $this->assertNotNull($food->getCreated()); - $this->assertNotNull($food->getUpdated()); + static::assertSame(1, $left); + static::assertNotNull($food->getCreated()); + static::assertNotNull($food->getUpdated()); - $translationRepo = $this->em->getRepository(self::TRANSLATION); + $translationRepo = $this->em->getRepository(Translation::class); $translations = $translationRepo->findTranslations($food); - $this->assertCount(0, $translations); - $this->assertEquals('food', $food->getSlug()); + static::assertCount(0, $translations); + static::assertSame('food', $food->getSlug()); } /** @@ -51,65 +56,65 @@ public function testInheritance() * Child count is invalid resulting in SINGLE_TABLE and JOINED * inheritance mapping. Also getChildren, getPath results are invalid */ - public function testCaseGithubIssue7() + public function testCaseGithubIssue7(): void { - $repo = $this->em->getRepository(self::NODE); - $vegies = $repo->findOneByTitle('Vegitables'); + $repo = $this->em->getRepository(Node::class); + $vegies = $repo->findOneBy(['title' => 'Vegitables']); - $count = $repo->childCount($vegies, true/*direct*/); - $this->assertEquals(3, $count); + $count = $repo->childCount($vegies, true/* direct */); + static::assertSame(3, $count); $children = $repo->children($vegies, true); - $this->assertCount(3, $children); + static::assertCount(3, $children); // node repository will not find it - $baseNodeRepo = $this->em->getRepository(self::BASE_NODE); - $cabbage = $baseNodeRepo->findOneByIdentifier('cabbage'); + $baseNodeRepo = $this->em->getRepository(BaseNode::class); + $cabbage = $baseNodeRepo->findOneBy(['identifier' => 'cabbage']); $path = $baseNodeRepo->getPath($cabbage); - $this->assertCount(3, $path); + static::assertCount(3, $path); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::NODE, - self::ANODE, - self::TRANSLATION, - self::BASE_NODE, - ); + return [ + Node::class, + ANode::class, + Translation::class, + BaseNode::class, + ]; } - private function populate() + private function populate(): void { - $root = new \Tree\Fixture\Node(); - $root->setTitle("Food"); + $root = new Node(); + $root->setTitle('Food'); $root->setIdentifier('food'); - $root2 = new \Tree\Fixture\Node(); - $root2->setTitle("Sports"); + $root2 = new Node(); + $root2->setTitle('Sports'); $root2->setIdentifier('sport'); - $child = new \Tree\Fixture\Node(); - $child->setTitle("Fruits"); + $child = new Node(); + $child->setTitle('Fruits'); $child->setParent($root); $child->setIdentifier('fruit'); - $child2 = new \Tree\Fixture\Node(); - $child2->setTitle("Vegitables"); + $child2 = new Node(); + $child2->setTitle('Vegitables'); $child2->setParent($root); $child2->setIdentifier('vegie'); - $childsChild = new \Tree\Fixture\Node(); - $childsChild->setTitle("Carrots"); + $childsChild = new Node(); + $childsChild->setTitle('Carrots'); $childsChild->setParent($child2); $childsChild->setIdentifier('carrot'); - $potatoes = new \Tree\Fixture\Node(); - $potatoes->setTitle("Potatoes"); + $potatoes = new Node(); + $potatoes->setTitle('Potatoes'); $potatoes->setParent($child2); $potatoes->setIdentifier('potatoe'); - $cabbages = new \Tree\Fixture\BaseNode(); + $cabbages = new BaseNode(); $cabbages->setIdentifier('cabbage'); $cabbages->setParent($child2); diff --git a/tests/Gedmo/Tree/MultiInheritanceWithSingleTableTest.php b/tests/Gedmo/Tree/MultiInheritanceWithSingleTableTest.php index 5c39e7ba06..a330caec41 100644 --- a/tests/Gedmo/Tree/MultiInheritanceWithSingleTableTest.php +++ b/tests/Gedmo/Tree/MultiInheritanceWithSingleTableTest.php @@ -1,63 +1,68 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\Transport\Car; -use Tree\Fixture\Transport\Bus; -use Tree\Fixture\Transport\Vehicle; -use Tree\Fixture\Transport\Engine; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Transport\Bus; +use Gedmo\Tests\Tree\Fixture\Transport\Car; +use Gedmo\Tests\Tree\Fixture\Transport\Engine; +use Gedmo\Tests\Tree\Fixture\Transport\Vehicle; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MultiInheritanceWithSingleTableTest extends BaseTestCaseORM +final class MultiInheritanceWithSingleTableTest extends BaseTestCaseORM { - const CAR = "Tree\Fixture\Transport\Car"; - const BUS = "Tree\Fixture\Transport\Bus"; - const VEHICLE = "Tree\Fixture\Transport\Vehicle"; - const ENGINE = "Tree\Fixture\Transport\Engine"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testConsistence() + public function testConsistence(): void { $this->populate(); - $carRepo = $this->em->getRepository(self::CAR); - $audi = $carRepo->findOneByTitle('Audi-80'); - $this->assertEquals(2, $carRepo->childCount($audi)); - $this->assertEquals(1, $audi->getLeft()); - $this->assertEquals(6, $audi->getRight()); + $this->em->clear(); + + $carRepo = $this->em->getRepository(Car::class); + $audi = $carRepo->findOneBy(['title' => 'Audi-80']); + static::assertSame(2, $carRepo->childCount($audi)); + static::assertSame(1, $audi->getLeft()); + static::assertSame(6, $audi->getRight()); $children = $carRepo->children($audi); - $this->assertCount(2, $children); + static::assertCount(2, $children); $path = $carRepo->getPath($children[0]); - $this->assertCount(2, $path); + static::assertCount(2, $path); $carRepo->moveDown($children[0]); - $this->assertEquals(4, $children[0]->getLeft()); - $this->assertEquals(5, $children[0]->getRight()); + static::assertSame(4, $children[0]->getLeft()); + static::assertSame(5, $children[0]->getRight()); - $this->assertTrue($carRepo->verify()); + static::assertTrue($carRepo->verify()); } /*public function testHeavyLoad() { - $carRepo = $this->em->getRepository(self::CAR); + $carRepo = $this->em->getRepository(Car::class); $parent = null; $num = 100; for($i = 0; $i < 100; $i++) { @@ -93,17 +98,17 @@ public function testConsistence() var_dump('processed: '.$num); }*/ - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::VEHICLE, - self::CAR, - self::ENGINE, - self::BUS, - ); + return [ + Vehicle::class, + Car::class, + Engine::class, + Bus::class, + ]; } - private function populate() + private function populate(): void { // engines $v8 = new Engine(); diff --git a/tests/Gedmo/Tree/NestedTreePositionTest.php b/tests/Gedmo/Tree/NestedTreePositionTest.php index a1c8af403f..d6de3691f0 100644 --- a/tests/Gedmo/Tree/NestedTreePositionTest.php +++ b/tests/Gedmo/Tree/NestedTreePositionTest.php @@ -1,38 +1,40 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\Category; -use Tree\Fixture\RootCategory; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Category; +use Gedmo\Tests\Tree\Fixture\RootCategory; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class NestedTreePositionTest extends BaseTestCaseORM +final class NestedTreePositionTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\Category"; - const ROOT_CATEGORY = "Tree\\Fixture\\RootCategory"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - /** - * @test - */ - public function shouldFailToPersistRootSibling() + public function testShouldFailToPersistRootSibling(): void { $food = new Category(); $food->setTitle('Food'); @@ -40,30 +42,27 @@ public function shouldFailToPersistRootSibling() $sport = new Category(); $sport->setTitle('Sport'); - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(Category::class); $repo->persistAsFirstChild($food); $repo->persistAsNextSiblingOf($sport, $food); $this->em->flush(); - $this->assertSame(0, $sport->getLevel()); - $this->assertSame(3, $sport->getLeft()); - $this->assertSame(4, $sport->getRight()); + static::assertSame(0, $sport->getLevel()); + static::assertSame(3, $sport->getLeft()); + static::assertSame(4, $sport->getRight()); } - /** - * @test - * @expectedException UnexpectedValueException - */ - public function shouldFailToPersistRootAsSiblingForRootBasedTree() + public function testShouldFailToPersistRootAsSiblingForRootBasedTree(): void { + $this->expectException('UnexpectedValueException'); $food = new RootCategory(); $food->setTitle('Food'); $sport = new RootCategory(); $sport->setTitle('Sport'); - $repo = $this->em->getRepository(self::ROOT_CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); $repo->persistAsFirstChild($food); $repo->persistAsNextSiblingOf($sport, $food); @@ -71,132 +70,132 @@ public function shouldFailToPersistRootAsSiblingForRootBasedTree() $this->em->flush(); } - public function testTreeChildPositionMove2() + public function testTreeChildPositionMove2(): void { $this->populate(); - $repo = $this->em->getRepository(self::ROOT_CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); - $oranges = $repo->findOneByTitle('Oranges'); - $meat = $repo->findOneByTitle('Meat'); + $oranges = $repo->findOneBy(['title' => 'Oranges']); + $meat = $repo->findOneBy(['title' => 'Meat']); - $this->assertEquals(2, $oranges->getLevel()); - $this->assertEquals(7, $oranges->getLeft()); - $this->assertEquals(8, $oranges->getRight()); + static::assertSame(3, $oranges->getLevel()); + static::assertSame(7, $oranges->getLeft()); + static::assertSame(8, $oranges->getRight()); $repo->persistAsNextSiblingOf($meat, $oranges); $this->em->flush(); - $oranges = $repo->findOneByTitle('Oranges'); - $meat = $repo->findOneByTitle('Meat'); + $oranges = $repo->findOneBy(['title' => 'Oranges']); + $meat = $repo->findOneBy(['title' => 'Meat']); - $this->assertEquals(7, $oranges->getLeft()); - $this->assertEquals(8, $oranges->getRight()); + static::assertSame(7, $oranges->getLeft()); + static::assertSame(8, $oranges->getRight()); - //Normal test that pass - $this->assertEquals(9, $meat->getLeft()); - $this->assertEquals(10, $meat->getRight()); + // Normal test that pass + static::assertSame(9, $meat->getLeft()); + static::assertSame(10, $meat->getRight()); // Raw query to show the issue #108 with wrong left value by Doctrine - $dql = 'SELECT c FROM '.self::ROOT_CATEGORY.' c'; - $dql .= ' WHERE c.id = 5'; //5 == meat + $dql = 'SELECT c FROM '.RootCategory::class.' c'; + $dql .= ' WHERE c.id = 5'; // 5 == meat $meat_array = $this->em->createQuery($dql)->getScalarResult(); - $this->assertEquals(9, $meat_array[0]['c_lft']); - $this->assertEquals(10, $meat_array[0]['c_rgt']); - $this->assertEquals(2, $meat_array[0]['c_level']); + static::assertSame(9, $meat_array[0]['c_lft']); + static::assertSame(10, $meat_array[0]['c_rgt']); + static::assertSame(3, $meat_array[0]['c_level']); } - public function testTreeChildPositionMove3() + public function testTreeChildPositionMove3(): void { $this->populate(); - $repo = $this->em->getRepository(self::ROOT_CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); - $oranges = $repo->findOneByTitle('Oranges'); - $milk = $repo->findOneByTitle('Milk'); + $oranges = $repo->findOneBy(['title' => 'Oranges']); + $milk = $repo->findOneBy(['title' => 'Milk']); - $this->assertEquals(2, $oranges->getLevel()); - $this->assertEquals(7, $oranges->getLeft()); - $this->assertEquals(8, $oranges->getRight()); + static::assertSame(3, $oranges->getLevel()); + static::assertSame(7, $oranges->getLeft()); + static::assertSame(8, $oranges->getRight()); $repo->persistAsNextSiblingOf($milk, $oranges); $this->em->flush(); - $this->assertEquals(7, $oranges->getLeft()); - $this->assertEquals(8, $oranges->getRight()); + static::assertSame(7, $oranges->getLeft()); + static::assertSame(8, $oranges->getRight()); - //Normal test that pass - $this->assertEquals(9, $milk->getLeft()); - $this->assertEquals(10, $milk->getRight()); + // Normal test that pass + static::assertSame(9, $milk->getLeft()); + static::assertSame(10, $milk->getRight()); // Raw query to show the issue #108 with wrong left value by Doctrine - $dql = 'SELECT c FROM '.self::ROOT_CATEGORY.' c'; - $dql .= ' WHERE c.id = 4 '; //4 == Milk + $dql = 'SELECT c FROM '.RootCategory::class.' c'; + $dql .= ' WHERE c.id = 4 '; // 4 == Milk $milk_array = $this->em->createQuery($dql)->getScalarResult(); - $this->assertEquals(9, $milk_array[0]['c_lft']); - $this->assertEquals(10, $milk_array[0]['c_rgt']); - $this->assertEquals(2, $milk_array[0]['c_level']); + static::assertSame(9, $milk_array[0]['c_lft']); + static::assertSame(10, $milk_array[0]['c_rgt']); + static::assertSame(3, $milk_array[0]['c_level']); } - public function testPositionedUpdates() + public function testPositionedUpdates(): void { $this->populate(); - $repo = $this->em->getRepository(self::ROOT_CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); - $citrons = $repo->findOneByTitle('Citrons'); - $vegitables = $repo->findOneByTitle('Vegitables'); + $citrons = $repo->findOneBy(['title' => 'Citrons']); + $vegitables = $repo->findOneBy(['title' => 'Vegitables']); $repo->persistAsNextSiblingOf($vegitables, $citrons); $this->em->flush(); - $this->assertEquals(5, $vegitables->getLeft()); - $this->assertEquals(6, $vegitables->getRight()); - $this->assertEquals(2, $vegitables->getParent()->getId()); + static::assertSame(5, $vegitables->getLeft()); + static::assertSame(6, $vegitables->getRight()); + static::assertSame(2, $vegitables->getParent()->getId()); - $fruits = $repo->findOneByTitle('Fruits'); - $this->assertEquals(2, $fruits->getLeft()); - $this->assertEquals(9, $fruits->getRight()); + $fruits = $repo->findOneBy(['title' => 'Fruits']); + static::assertSame(2, $fruits->getLeft()); + static::assertSame(9, $fruits->getRight()); - $milk = $repo->findOneByTitle('Milk'); + $milk = $repo->findOneBy(['title' => 'Milk']); $repo->persistAsFirstChildOf($milk, $fruits); $this->em->flush(); - $this->assertEquals(3, $milk->getLeft()); - $this->assertEquals(4, $milk->getRight()); + static::assertSame(3, $milk->getLeft()); + static::assertSame(4, $milk->getRight()); - $this->assertEquals(2, $fruits->getLeft()); - $this->assertEquals(11, $fruits->getRight()); + static::assertSame(2, $fruits->getLeft()); + static::assertSame(11, $fruits->getRight()); } - public function testTreeChildPositionMove() + public function testTreeChildPositionMove(): void { $this->populate(); - $repo = $this->em->getRepository(self::ROOT_CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); - $oranges = $repo->findOneByTitle('Oranges'); - $fruits = $repo->findOneByTitle('Fruits'); + $oranges = $repo->findOneBy(['title' => 'Oranges']); + $fruits = $repo->findOneBy(['title' => 'Fruits']); - $this->assertEquals(2, $oranges->getLevel()); + static::assertSame(3, $oranges->getLevel()); $repo->persistAsNextSiblingOf($oranges, $fruits); $this->em->flush(); - $this->assertEquals(1, $oranges->getLevel()); - $this->assertCount(1, $repo->children($fruits, true)); + static::assertSame(2, $oranges->getLevel()); + static::assertCount(1, $repo->children($fruits, true)); - $vegies = $repo->findOneByTitle('Vegitables'); - $this->assertEquals(2, $vegies->getLeft()); + $vegies = $repo->findOneBy(['title' => 'Vegitables']); + static::assertSame(2, $vegies->getLeft()); $repo->persistAsNextSiblingOf($vegies, $fruits); $this->em->flush(); - $this->assertEquals(6, $vegies->getLeft()); + static::assertSame(6, $vegies->getLeft()); $this->em->flush(); - $this->assertEquals(6, $vegies->getLeft()); + static::assertSame(6, $vegies->getLeft()); } - public function testOnRootCategory() + public function testOnRootCategory(): void { // need to check if this does not produce errors - $repo = $this->em->getRepository(self::ROOT_CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); $fruits = new RootCategory(); $fruits->setTitle('Fruits'); @@ -227,12 +226,12 @@ public function testOnRootCategory() ->persistAsPrevSibling($drinks); $this->em->flush(); - $dql = 'SELECT COUNT(c) FROM '.self::ROOT_CATEGORY.' c'; - $dql .= ' WHERE c.lft = 1 AND c.rgt = 2 AND c.parent IS NULL AND c.level = 0'; + $dql = 'SELECT COUNT(c) FROM '.RootCategory::class.' c'; + $dql .= ' WHERE c.lft = 1 AND c.rgt = 2 AND c.parent IS NULL AND c.level = 1'; $count = $this->em->createQuery($dql)->getSingleScalarResult(); - $this->assertEquals(6, $count); + static::assertSame(6, (int) $count); - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(Category::class); $fruits = new Category(); $fruits->setTitle('Fruits'); @@ -263,16 +262,16 @@ public function testOnRootCategory() ->persistAsPrevSibling($drinks); $this->em->flush(); - $dql = 'SELECT COUNT(c) FROM '.self::CATEGORY.' c'; + $dql = 'SELECT COUNT(c) FROM '.Category::class.' c'; $dql .= ' WHERE c.parentId IS NULL AND c.level = 0'; $dql .= ' AND c.lft BETWEEN 1 AND 11'; $count = $this->em->createQuery($dql)->getSingleScalarResult(); - $this->assertEquals(6, $count); + static::assertSame(6, (int) $count); } - public function testRootTreePositionedInserts() + public function testRootTreePositionedInserts(): void { - $repo = $this->em->getRepository(self::ROOT_CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); // test child positioned inserts $food = new RootCategory(); @@ -299,17 +298,17 @@ public function testRootTreePositionedInserts() $this->em->flush(); - $this->assertEquals(4, $fruits->getLeft()); - $this->assertEquals(5, $fruits->getRight()); + static::assertSame(4, $fruits->getLeft()); + static::assertSame(5, $fruits->getRight()); - $this->assertEquals(2, $vegitables->getLeft()); - $this->assertEquals(3, $vegitables->getRight()); + static::assertSame(2, $vegitables->getLeft()); + static::assertSame(3, $vegitables->getRight()); - $this->assertEquals(6, $milk->getLeft()); - $this->assertEquals(7, $milk->getRight()); + static::assertSame(6, $milk->getLeft()); + static::assertSame(7, $milk->getRight()); - $this->assertEquals(8, $meat->getLeft()); - $this->assertEquals(9, $meat->getRight()); + static::assertSame(8, $meat->getLeft()); + static::assertSame(9, $meat->getRight()); // test sibling positioned inserts $cookies = new RootCategory(); @@ -324,18 +323,77 @@ public function testRootTreePositionedInserts() $this->em->flush(); - $this->assertEquals(6, $drinks->getLeft()); - $this->assertEquals(7, $drinks->getRight()); + static::assertSame(6, $drinks->getLeft()); + static::assertSame(7, $drinks->getRight()); + + static::assertSame(10, $cookies->getLeft()); + static::assertSame(11, $cookies->getRight()); + + static::assertTrue($repo->verify()); + } + + public function testRootlessTreeTopLevelInserts(): void + { + $repo = $this->em->getRepository(Category::class); + + // test top level positioned inserts + $fruits = new Category(); + $fruits->setTitle('Fruits'); + + $vegetables = new Category(); + $vegetables->setTitle('Vegetables'); + + $milk = new Category(); + $milk->setTitle('Milk'); + + $meat = new Category(); + $meat->setTitle('Meat'); + + $repo + ->persistAsFirstChild($fruits) + ->persistAsFirstChild($vegetables) + ->persistAsLastChild($milk) + ->persistAsLastChild($meat); + + $this->em->flush(); + + static::assertSame(3, $fruits->getLeft()); + static::assertSame(4, $fruits->getRight()); + + static::assertSame(1, $vegetables->getLeft()); + static::assertSame(2, $vegetables->getRight()); + + static::assertSame(5, $milk->getLeft()); + static::assertSame(6, $milk->getRight()); + + static::assertSame(7, $meat->getLeft()); + static::assertSame(8, $meat->getRight()); + + // test sibling positioned inserts + $cookies = new Category(); + $cookies->setTitle('Cookies'); + + $drinks = new Category(); + $drinks->setTitle('Drinks'); + + $repo + ->persistAsNextSiblingOf($cookies, $milk) + ->persistAsPrevSiblingOf($drinks, $milk); + + $this->em->flush(); + + static::assertSame(5, $drinks->getLeft()); + static::assertSame(6, $drinks->getRight()); - $this->assertEquals(10, $cookies->getLeft()); - $this->assertEquals(11, $cookies->getRight()); + static::assertSame(9, $cookies->getLeft()); + static::assertSame(10, $cookies->getRight()); - $this->assertTrue($repo->verify()); + static::assertTrue($repo->verify()); } - public function testSimpleTreePositionedInserts() + public function testSimpleTreePositionedInserts(): void { - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(Category::class); // test child positioned inserts $food = new Category(); @@ -364,17 +422,17 @@ public function testSimpleTreePositionedInserts() $this->em->flush(); - $this->assertEquals(4, $fruits->getLeft()); - $this->assertEquals(5, $fruits->getRight()); + static::assertSame(4, $fruits->getLeft()); + static::assertSame(5, $fruits->getRight()); - $this->assertEquals(2, $vegitables->getLeft()); - $this->assertEquals(3, $vegitables->getRight()); + static::assertSame(2, $vegitables->getLeft()); + static::assertSame(3, $vegitables->getRight()); - $this->assertEquals(6, $milk->getLeft()); - $this->assertEquals(7, $milk->getRight()); + static::assertSame(6, $milk->getLeft()); + static::assertSame(7, $milk->getRight()); - $this->assertEquals(8, $meat->getLeft()); - $this->assertEquals(9, $meat->getRight()); + static::assertSame(8, $meat->getLeft()); + static::assertSame(9, $meat->getRight()); // test sibling positioned inserts $cookies = new Category(); @@ -389,18 +447,26 @@ public function testSimpleTreePositionedInserts() $this->em->flush(); - $this->assertEquals(6, $drinks->getLeft()); - $this->assertEquals(7, $drinks->getRight()); + static::assertSame(6, $drinks->getLeft()); + static::assertSame(7, $drinks->getRight()); - $this->assertEquals(10, $cookies->getLeft()); - $this->assertEquals(11, $cookies->getRight()); + static::assertSame(10, $cookies->getLeft()); + static::assertSame(11, $cookies->getRight()); - $this->assertTrue($repo->verify()); + static::assertTrue($repo->verify()); } - private function populate() + protected function getUsedEntityFixtures(): array { - $repo = $this->em->getRepository(self::ROOT_CATEGORY); + return [ + Category::class, + RootCategory::class, + ]; + } + + private function populate(): void + { + $repo = $this->em->getRepository(RootCategory::class); $food = new RootCategory(); $food->setTitle('Food'); @@ -434,12 +500,4 @@ private function populate() $this->em->flush(); } - - protected function getUsedEntityFixtures() - { - return array( - self::CATEGORY, - self::ROOT_CATEGORY, - ); - } } diff --git a/tests/Gedmo/Tree/NestedTreeRootAssociationTest.php b/tests/Gedmo/Tree/NestedTreeRootAssociationTest.php new file mode 100644 index 0000000000..00bc94ec18 --- /dev/null +++ b/tests/Gedmo/Tree/NestedTreeRootAssociationTest.php @@ -0,0 +1,137 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\RootAssociationCategory; +use Gedmo\Tree\TreeListener; + +/** + * These are tests for Tree behavior + * + * @author Gediminas Morkevicius + */ +final class NestedTreeRootAssociationTest extends BaseTestCaseORM +{ + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $evm->addEventSubscriber(new TreeListener()); + + $this->getDefaultMockSqliteEntityManager($evm); + $this->populate(); + } + + public function testRootEntity(): void + { + $repo = $this->em->getRepository(RootAssociationCategory::class); + + // Foods + $food = $repo->findOneBy(['title' => 'Food']); + static::assertSame($food->getId(), $food->getRoot()->getId()); + + $fruits = $repo->findOneBy(['title' => 'Fruits']); + static::assertSame($food->getId(), $fruits->getRoot()->getId()); + + $vegetables = $repo->findOneBy(['title' => 'Vegetables']); + static::assertSame($food->getId(), $vegetables->getRoot()->getId()); + + $carrots = $repo->findOneBy(['title' => 'Carrots']); + static::assertSame($food->getId(), $carrots->getRoot()->getId()); + + $potatoes = $repo->findOneBy(['title' => 'Potatoes']); + static::assertSame($food->getId(), $potatoes->getRoot()->getId()); + + // Sports + $sports = $repo->findOneBy(['title' => 'Sports']); + static::assertSame($sports->getId(), $sports->getRoot()->getId()); + } + + public function testRemoveParentForNode(): void + { + $repo = $this->em->getRepository(RootAssociationCategory::class); + + /** @var RootAssociationCategory $food */ + $food = $repo->findOneBy(['title' => 'Food']); + static::assertSame($food->getId(), $food->getRoot()->getId()); + static::assertSame(0, $food->getLevel()); + static::assertSame(1, $food->getLeft()); + static::assertSame(10, $food->getRight()); + + /** @var RootAssociationCategory $fruits */ + $fruits = $repo->findOneBy(['title' => 'Fruits']); + static::assertSame($food->getId(), $fruits->getRoot()->getId()); + static::assertSame(1, $fruits->getLevel()); + static::assertSame(2, $fruits->getLeft()); + static::assertSame(3, $fruits->getRight()); + + // Remove node's parent, which should move out the node into a new tree + $fruits->setParent(null); + $this->em->flush(); + + $food = $repo->findOneBy(['title' => 'Food']); + static::assertSame($food->getId(), $food->getRoot()->getId()); + static::assertSame(0, $food->getLevel()); + static::assertSame(1, $food->getLeft()); + static::assertSame(8, $food->getRight()); + + $fruits = $repo->findOneBy(['title' => 'Fruits']); + static::assertSame($fruits->getId(), $fruits->getRoot()->getId()); + static::assertSame(0, $fruits->getLevel()); + static::assertSame(1, $fruits->getLeft()); + static::assertSame(2, $fruits->getRight()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + RootAssociationCategory::class, + ]; + } + + private function populate(): void + { + $root = new RootAssociationCategory(); + $root->setTitle('Food'); + + $root2 = new RootAssociationCategory(); + $root2->setTitle('Sports'); + + $child = new RootAssociationCategory(); + $child->setTitle('Fruits'); + $child->setParent($root); + + $child2 = new RootAssociationCategory(); + $child2->setTitle('Vegetables'); + $child2->setParent($root); + + $childsChild = new RootAssociationCategory(); + $childsChild->setTitle('Carrots'); + $childsChild->setParent($child2); + + $potatoes = new RootAssociationCategory(); + $potatoes->setTitle('Potatoes'); + $potatoes->setParent($child2); + + $this->em->persist($root); + $this->em->persist($root2); + $this->em->persist($child); + $this->em->persist($child2); + $this->em->persist($childsChild); + $this->em->persist($potatoes); + $this->em->flush(); + $this->em->clear(); + } +} diff --git a/tests/Gedmo/Tree/NestedTreeRootRepositoryTest.php b/tests/Gedmo/Tree/NestedTreeRootRepositoryTest.php index b9e6148951..5b624fe755 100644 --- a/tests/Gedmo/Tree/NestedTreeRootRepositoryTest.php +++ b/tests/Gedmo/Tree/NestedTreeRootRepositoryTest.php @@ -1,43 +1,49 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\RootCategory; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\RootCategory; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class NestedTreeRootRepositoryTest extends BaseTestCaseORM +final class NestedTreeRootRepositoryTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\RootCategory"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } /** * Based on issue #342 - * - * @test */ - public function shouldBeAbleToShiftRootNode() + public function testShouldBeAbleToShiftRootNode(): void { - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $acme = new RootCategory(); $acme->setTitle('Acme'); @@ -47,107 +53,102 @@ public function shouldBeAbleToShiftRootNode() $this->em->persist($food); $this->em->flush(); - $this->assertNull($acme->getParent()); - $this->assertSame($acme, $food->getParent()); - $this->assertSame($acme->getId(), $acme->getRoot()); - $this->assertSame($acme->getId(), $food->getRoot()); - $this->assertSame(1, $acme->getLeft()); - $this->assertSame(12, $acme->getRight()); - $this->assertSame(2, $food->getLeft()); - $this->assertSame(11, $food->getRight()); + static::assertNull($acme->getParent()); + static::assertSame($acme, $food->getParent()); + static::assertSame($acme->getId(), $acme->getRoot()); + static::assertSame($acme->getId(), $food->getRoot()); + static::assertSame(1, $acme->getLeft()); + static::assertSame(12, $acme->getRight()); + static::assertSame(2, $food->getLeft()); + static::assertSame(11, $food->getRight()); } - /** - * @test - */ - public function shouldSupportChildrenHierarchyAsArray() + public function testShouldSupportChildrenHierarchyAsArray(): void { - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); $result = $repo->childrenHierarchy(); - $this->assertCount(2, $result); - $this->assertTrue(isset($result[0]['__children'][0]['__children'])); + static::assertCount(2, $result); + static::assertTrue(isset($result[0]['__children'][0]['__children'])); - $vegies = $repo->findOneByTitle('Vegitables'); + $vegies = $repo->findOneBy(['title' => 'Vegitables']); $result = $repo->childrenHierarchy($vegies); - $this->assertCount(2, $result); - $this->assertCount(0, $result[0]['__children']); + static::assertCount(2, $result); + static::assertCount(0, $result[0]['__children']); // Complete Tree $roots = $repo->getRootNodes(); $tree = $repo->childrenHierarchy(); - $this->assertEquals(2, count($tree)); // Count roots - $this->assertEquals('Food', $tree[0]['title']); - $this->assertEquals('Sports', $tree[1]['title']); - $this->assertEquals('Fruits', $tree[0]['__children'][0]['title']); - $this->assertEquals('Vegitables', $tree[0]['__children'][1]['title']); - $this->assertEquals('Carrots', $tree[0]['__children'][1]['__children'][0]['title']); - $this->assertEquals('Potatoes', $tree[0]['__children'][1]['__children'][1]['title']); + static::assertCount(2, $tree); // Count roots + static::assertSame('Food', $tree[0]['title']); + static::assertSame('Sports', $tree[1]['title']); + static::assertSame('Fruits', $tree[0]['__children'][0]['title']); + static::assertSame('Vegitables', $tree[0]['__children'][1]['title']); + static::assertSame('Carrots', $tree[0]['__children'][1]['__children'][0]['title']); + static::assertSame('Potatoes', $tree[0]['__children'][1]['__children'][1]['title']); // Tree of one specific root, without the root node $roots = $repo->getRootNodes(); $tree = $repo->childrenHierarchy($roots[0]); - $this->assertEquals(2, count($tree)); // Count roots - $this->assertEquals('Fruits', $tree[0]['title']); - $this->assertEquals('Vegitables', $tree[1]['title']); - $this->assertEquals('Carrots', $tree[1]['__children'][0]['title']); - $this->assertEquals('Potatoes', $tree[1]['__children'][1]['title']); + static::assertCount(2, $tree); // Count roots + static::assertSame('Fruits', $tree[0]['title']); + static::assertSame('Vegitables', $tree[1]['title']); + static::assertSame('Carrots', $tree[1]['__children'][0]['title']); + static::assertSame('Potatoes', $tree[1]['__children'][1]['title']); // Tree of one specific root, with the root node - $tree = $repo->childrenHierarchy($roots[0], false, array(), true); + $tree = $repo->childrenHierarchy($roots[0], false, [], true); - $this->assertEquals(1, count($tree)); // Count roots - $this->assertEquals('Food', $tree[0]['title']); - $this->assertEquals('Fruits', $tree[0]['__children'][0]['title']); - $this->assertEquals('Vegitables', $tree[0]['__children'][1]['title']); - $this->assertEquals('Carrots', $tree[0]['__children'][1]['__children'][0]['title']); - $this->assertEquals('Potatoes', $tree[0]['__children'][1]['__children'][1]['title']); + static::assertCount(1, $tree); // Count roots + static::assertSame('Food', $tree[0]['title']); + static::assertSame('Fruits', $tree[0]['__children'][0]['title']); + static::assertSame('Vegitables', $tree[0]['__children'][1]['title']); + static::assertSame('Carrots', $tree[0]['__children'][1]['__children'][0]['title']); + static::assertSame('Potatoes', $tree[0]['__children'][1]['__children'][1]['title']); // Tree of one specific root only with direct children, without the root node $roots = $repo->getRootNodes(); $tree = $repo->childrenHierarchy($roots[0], true); - $this->assertEquals(2, count($tree)); - $this->assertEquals('Fruits', $tree[0]['title']); - $this->assertEquals('Vegitables', $tree[1]['title']); + static::assertCount(2, $tree); + static::assertSame('Fruits', $tree[0]['title']); + static::assertSame('Vegitables', $tree[1]['title']); // Tree of one specific root only with direct children, with the root node - $tree = $repo->childrenHierarchy($roots[0], true, array(), true); + $tree = $repo->childrenHierarchy($roots[0], true, [], true); - $this->assertEquals(1, count($tree)); - $this->assertEquals('Food', $tree[0]['title']); - $this->assertEquals('Fruits', $tree[0]['__children'][0]['title']); - $this->assertEquals('Vegitables', $tree[0]['__children'][1]['title']); + static::assertCount(1, $tree); + static::assertSame('Food', $tree[0]['title']); + static::assertSame('Fruits', $tree[0]['__children'][0]['title']); + static::assertSame('Vegitables', $tree[0]['__children'][1]['title']); } - /** - * @test - */ - public function shouldSupportChildrenHierarchyAsHtml() + public function testShouldSupportChildrenHierarchyAsHtml(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $food = $repo->findOneByTitle('Food'); + $repo = $this->em->getRepository(RootCategory::class); + $food = $repo->findOneBy(['title' => 'Food']); $decorate = true; - $defaultHtmlTree = $repo->childrenHierarchy($food, false, compact('decorate')); + $defaultHtmlTree = $repo->childrenHierarchy($food, false, ['decorate' => $decorate]); - $this->assertEquals( + static::assertSame( '
            • Fruits
            • Vegitables
              • Carrots
              • Potatoes
            ', $defaultHtmlTree ); // custom title - $nodeDecorator = function ($node) { - return ''.$node['title'].''; - }; + $nodeDecorator = static fn ($node) => ''.$node['title'].''; $decoratedHtmlTree = $repo->childrenHierarchy( $food, false, - compact('decorate', 'nodeDecorator') + [ + 'decorate' => $decorate, + 'nodeDecorator' => $nodeDecorator, + ] ); - $this->assertEquals( + static::assertSame( '
            • Fruits
            • Vegitables
              • Carrots
              • Potatoes
            ', $decoratedHtmlTree ); @@ -156,301 +157,437 @@ public function shouldSupportChildrenHierarchyAsHtml() $rootClose = ''; $childOpen = ''; $childClose = ''; - $nodeDecorator = function ($node) { - return str_repeat('-', $node['level']).$node['title']."\n"; - }; + $nodeDecorator = static fn ($node) => str_repeat('-', $node['level'] - 1).$node['title']."\n"; $decoratedCliTree = $repo->childrenHierarchy( $food, false, - compact('decorate', 'nodeDecorator', 'rootOpen', 'rootClose', 'childOpen', 'childClose') + [ + 'decorate' => $decorate, + 'nodeDecorator' => $nodeDecorator, + 'rootOpen' => $rootOpen, + 'rootClose' => $rootClose, + 'childOpen' => $childOpen, + 'childClose' => $childClose, + ] ); - $this->assertEquals( + static::assertSame( "-Fruits\n-Vegitables\n--Carrots\n--Potatoes\n", $decoratedCliTree ); - $rootOpen = function () {return '
              ';}; + $rootOpen = static fn () => '
                '; // check support of the closures in rootClose - $rootClose = function () {return '
              ';}; - $childOpen = function (&$node) { - return '
            • '; - }; + $rootClose = static fn () => '
            '; + $childOpen = static fn (&$node) => '
          • '; // check support of the closures in childClose - $childClose = function (&$node) { - return '
          • '; - }; + $childClose = static fn (&$node) => ''; $decoratedHtmlTree = $repo->childrenHierarchy( $food, false, - compact('decorate', 'rootOpen', 'rootClose','childOpen','childClose') + [ + 'decorate' => $decorate, + 'rootOpen' => $rootOpen, + 'rootClose' => $rootClose, + 'childOpen' => $childOpen, + 'childClose' => $childClose, + ] ); - $this->assertEquals( + static::assertSame( '
            • Fruits
            • Vegitables
              • Carrots
              • Potatoes
            ', $decoratedHtmlTree ); } - /** - * @test - */ - public function shouldSupportChildrenHierarchyByBuildTreeFunction() + public function testShouldSupportChildrenHierarchyByBuildTreeFunction(): void { - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); $q = $this->em ->createQueryBuilder() ->select('node') - ->from(self::CATEGORY, 'node') + ->from(RootCategory::class, 'node') ->orderBy('node.root, node.lft', 'ASC') ->where('node.root = 1') ->getQuery() ; $tree = $repo->buildTree($q->getArrayResult()); - $this->assertCount(1, $tree); - $this->assertCount(2, $tree[0]['__children']); - $nodes = array(); - $options = array('decorate' => true); - $this->assertEquals('', $repo->buildTree($nodes, $options), 'should give empty string when there are no nodes given'); + static::assertCount(1, $tree); + static::assertCount(2, $tree[0]['__children']); + $nodes = []; + $options = ['decorate' => true]; + static::assertSame('', $repo->buildTree($nodes, $options), 'should give empty string when there are no nodes given'); } - /** - * @test - */ - public function shouldRemoveRootNodeFromTree() + public function testShouldRemoveRootNodeFromTree(): void { - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); $this->populateMore(); - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $repo->removeFromTree($food); $this->em->clear(); - $food = $repo->findOneByTitle('Food'); - $this->assertNull($food); + $food = $repo->findOneBy(['title' => 'Food']); + static::assertNull($food); - $node = $repo->findOneByTitle('Fruits'); + $node = $repo->findOneBy(['title' => 'Fruits']); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(2, $node->getRight()); - $this->assertEquals(3, $node->getRoot()); - $this->assertNull($node->getParent()); + static::assertSame(1, $node->getLeft()); + static::assertSame(2, $node->getRight()); + static::assertSame(3, $node->getRoot()); + static::assertNull($node->getParent()); - $node = $repo->findOneByTitle('Vegitables'); + $node = $repo->findOneBy(['title' => 'Vegitables']); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(10, $node->getRight()); - $this->assertEquals(4, $node->getRoot()); - $this->assertNull($node->getParent()); + static::assertSame(1, $node->getLeft()); + static::assertSame(10, $node->getRight()); + static::assertSame(4, $node->getRoot()); + static::assertNull($node->getParent()); } /** - * @test + * @dataProvider invalidStringMethods + * + * @param mixed $stringMethod */ - public function shouldHandleBasicRepositoryMethods() + public function testGetPathAsStringWithInvalidStringMethod($stringMethod): void { - $repo = $this->em->getRepository(self::CATEGORY); - $carrots = $repo->findOneByTitle('Carrots'); + $repo = $this->em->getRepository(RootCategory::class); + $carrots = $repo->findOneBy(['title' => 'Carrots']); - $path = $repo->getPath($carrots); - $this->assertCount(3, $path); - $this->assertEquals('Food', $path[0]->getTitle()); - $this->assertEquals('Vegitables', $path[1]->getTitle()); - $this->assertEquals('Carrots', $path[2]->getTitle()); + $this->expectException(InvalidArgumentException::class); + $repo->getPathAsString($carrots, [ + 'stringMethod' => $stringMethod, + ]); + } - $vegies = $repo->findOneByTitle('Vegitables'); + /** + * @phpstan-return iterable + */ + public static function invalidStringMethods(): iterable + { + yield [null]; + yield [123]; + yield ['nonExistingMethod']; + yield ['']; + } + + public function testShouldHandleBasicRepositoryMethods(): void + { + /** @var NestedTreeRepository $repo */ + $repo = $this->em->getRepository(RootCategory::class); + $carrots = $repo->findOneBy(['title' => 'Carrots']); + + $path = $repo->getPath($carrots); + static::assertCount(3, $path); + static::assertSame('Food', $path[0]->getTitle()); + static::assertSame('Vegitables', $path[1]->getTitle()); + static::assertSame('Carrots', $path[2]->getTitle()); + + $path = $repo->getPath($carrots, ['includeNode' => false]); + static::assertCount(2, $path); + static::assertSame('Food', $path[0]->getTitle()); + static::assertSame('Vegitables', $path[1]->getTitle()); + $path = $repo->getPathAsString($carrots, [ + 'includeNode' => true, + 'separator' => '-->', + 'stringMethod' => 'getTitle', + ]); + static::assertSame('Food-->Vegitables-->Carrots', $path); + + $vegies = $repo->findOneBy(['title' => 'Vegitables']); $childCount = $repo->childCount($vegies); - $this->assertEquals(2, $childCount); + static::assertSame(2, $childCount); - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $childCount = $repo->childCount($food, true); - $this->assertEquals(2, $childCount); + static::assertSame(2, $childCount); $childCount = $repo->childCount($food); - $this->assertEquals(4, $childCount); + static::assertSame(4, $childCount); $childCount = $repo->childCount(); - $this->assertEquals(6, $childCount); + static::assertSame(6, $childCount); $childCount = $repo->childCount(null, true); - $this->assertEquals(2, $childCount); + static::assertSame(2, $childCount); + + // all children of node, including the root, ordered by two fields + $food = $repo->findOneBy(['title' => 'Food']); + $children = $repo->children($food, false, ['level', 'title'], ['asc', 'desc'], true); + static::assertCount(5, $children); + static::assertSame('Food', $children[0]->getTitle()); + static::assertSame('Vegitables', $children[1]->getTitle()); + static::assertSame('Fruits', $children[2]->getTitle()); + static::assertSame('Potatoes', $children[3]->getTitle()); + static::assertSame('Carrots', $children[4]->getTitle()); } - /** - * @test - */ - public function shouldHandleAdvancedRepositoryFunctions() + public function testShouldHandleAdvancedRepositoryFunctions(): void { $this->populateMore(); - $repo = $this->em->getRepository(self::CATEGORY); + /** @var NestedTreeRepository $repo */ + $repo = $this->em->getRepository(RootCategory::class); // verification - $this->assertTrue($repo->verify()); + static::assertTrue($repo->verify()); - $dql = 'UPDATE '.self::CATEGORY.' node'; + $dql = 'UPDATE '.RootCategory::class.' node'; $dql .= ' SET node.lft = 5'; $dql .= ' WHERE node.id = 4'; $this->em->createQuery($dql)->getSingleScalarResult(); $this->em->clear(); // must clear cached entities $errors = $repo->verify(); - $this->assertCount(2, $errors); - $this->assertEquals('index [4], missing on tree root: 1', $errors[0]); - $this->assertEquals('index [5], duplicate on tree root: 1', $errors[1]); + static::assertIsArray($errors); + static::assertCount(2, $errors); + static::assertSame('index [4], missing on tree root: 1', $errors[0]); + static::assertSame('index [5], duplicate on tree root: 1', $errors[1]); + + // verification of single tree + $errors = $repo->verify(['treeRootNode' => $repo->find(2)]); + static::assertTrue($errors); + $errors = $repo->verify(['treeRootNode' => $repo->find(1)]); + static::assertCount(2, $errors); // test recover functionality $repo->recover(); $this->em->flush(); - $this->assertTrue($repo->verify()); + static::assertTrue($repo->verify()); $this->em->clear(); - $onions = $repo->findOneByTitle('Onions'); + $onions = $repo->findOneBy(['title' => 'Onions']); - $this->assertEquals(11, $onions->getLeft()); - $this->assertEquals(12, $onions->getRight()); + static::assertSame(11, $onions->getLeft()); + static::assertSame(12, $onions->getRight()); // move up $repo->moveUp($onions); - $this->assertEquals(9, $onions->getLeft()); - $this->assertEquals(10, $onions->getRight()); + static::assertSame(9, $onions->getLeft()); + static::assertSame(10, $onions->getRight()); $repo->moveUp($onions, true); - $this->assertEquals(5, $onions->getLeft()); - $this->assertEquals(6, $onions->getRight()); + static::assertSame(5, $onions->getLeft()); + static::assertSame(6, $onions->getRight()); // move down $repo->moveDown($onions, 2); - $this->assertEquals(9, $onions->getLeft()); - $this->assertEquals(10, $onions->getRight()); + static::assertSame(9, $onions->getLeft()); + static::assertSame(10, $onions->getRight()); + + // reorder (non-recursive) + + $node = $repo->findOneBy(['title' => 'Food']); + $repo->reorder($node, 'title', 'DESC', false, false); + + $node = $repo->findOneBy(['title' => 'Vegitables']); + + static::assertSame(2, $node->getLeft()); + static::assertSame(11, $node->getRight()); + + $node = $repo->findOneBy(['title' => 'Fruits']); + + static::assertSame(12, $node->getLeft()); + static::assertSame(13, $node->getRight()); + + $node = $repo->findOneBy(['title' => 'Carrots']); + + static::assertSame(3, $node->getLeft()); + static::assertSame(4, $node->getRight()); + + $node = $repo->findOneBy(['title' => 'Potatoes']); + + static::assertSame(5, $node->getLeft()); + static::assertSame(6, $node->getRight()); + + $node = $repo->findOneBy(['title' => 'Onions']); + + static::assertSame(7, $node->getLeft()); + static::assertSame(8, $node->getRight()); + + $node = $repo->findOneBy(['title' => 'Cabbages']); + + static::assertSame(9, $node->getLeft()); + static::assertSame(10, $node->getRight()); // reorder - $node = $repo->findOneByTitle('Food'); + $node = $repo->findOneBy(['title' => 'Food']); $repo->reorder($node, 'title', 'ASC', false); - $node = $repo->findOneByTitle('Cabbages'); + $node = $repo->findOneBy(['title' => 'Cabbages']); - $this->assertEquals(5, $node->getLeft()); - $this->assertEquals(6, $node->getRight()); + static::assertSame(5, $node->getLeft()); + static::assertSame(6, $node->getRight()); - $node = $repo->findOneByTitle('Carrots'); + $node = $repo->findOneBy(['title' => 'Carrots']); - $this->assertEquals(7, $node->getLeft()); - $this->assertEquals(8, $node->getRight()); + static::assertSame(7, $node->getLeft()); + static::assertSame(8, $node->getRight()); - $node = $repo->findOneByTitle('Onions'); + $node = $repo->findOneBy(['title' => 'Onions']); - $this->assertEquals(9, $node->getLeft()); - $this->assertEquals(10, $node->getRight()); + static::assertSame(9, $node->getLeft()); + static::assertSame(10, $node->getRight()); - $node = $repo->findOneByTitle('Potatoes'); + $node = $repo->findOneBy(['title' => 'Potatoes']); - $this->assertEquals(11, $node->getLeft()); - $this->assertEquals(12, $node->getRight()); + static::assertSame(11, $node->getLeft()); + static::assertSame(12, $node->getRight()); // leafs $leafs = $repo->getLeafs($node); - $this->assertCount(5, $leafs); - $this->assertEquals('Fruits', $leafs[0]->getTitle()); - $this->assertEquals('Cabbages', $leafs[1]->getTitle()); - $this->assertEquals('Carrots', $leafs[2]->getTitle()); - $this->assertEquals('Onions', $leafs[3]->getTitle()); - $this->assertEquals('Potatoes', $leafs[4]->getTitle()); + static::assertCount(5, $leafs); + static::assertSame('Fruits', $leafs[0]->getTitle()); + static::assertSame('Cabbages', $leafs[1]->getTitle()); + static::assertSame('Carrots', $leafs[2]->getTitle()); + static::assertSame('Onions', $leafs[3]->getTitle()); + static::assertSame('Potatoes', $leafs[4]->getTitle()); // remove - $node = $repo->findOneByTitle('Fruits'); + $node = $repo->findOneBy(['title' => 'Fruits']); $id = $node->getId(); $repo->removeFromTree($node); - $this->assertNull($repo->find($id)); + static::assertNull($repo->find($id)); - $node = $repo->findOneByTitle('Vegitables'); + $node = $repo->findOneBy(['title' => 'Vegitables']); $id = $node->getId(); $repo->removeFromTree($node); - $this->assertNull($repo->find($id)); + static::assertNull($repo->find($id)); + $this->em->clear(); + + $node = $repo->findOneBy(['title' => 'Cabbages']); + + static::assertSame(1, $node->getRoot()); + static::assertSame(1, $node->getParent()->getId()); + + // recover with specified order + + $repo->recover([ + 'flush' => true, + 'treeRootNode' => $repo->find(1), + 'skipVerify' => true, + 'sortByField' => 'title', + 'sortDirection' => 'DESC', + ]); + static::assertTrue($repo->verify()); + $this->em->clear(); + $potatoes = $repo->findOneBy(['title' => 'Potatoes']); + + static::assertSame(2, $potatoes->getLeft()); + static::assertSame(3, $potatoes->getRight()); + + // recover with specified order with multiple fields + + $repo->recover([ + 'flush' => true, + 'treeRootNode' => $repo->find(1), + 'skipVerify' => true, + 'sortByField' => [ + 0 => 'title', + 1 => 'title', + ], + 'sortDirection' => [ + 0 => 'ASC', + 1 => 'DESC', + ], + ]); + static::assertTrue($repo->verify()); - $node = $repo->findOneByTitle('Cabbages'); + $this->em->clear(); + $potatoes = $repo->findOneBy(['title' => 'Potatoes']); - $this->assertEquals(1, $node->getRoot()); - $this->assertEquals(1, $node->getParent()->getId()); + static::assertSame(8, $potatoes->getLeft()); + static::assertSame(9, $potatoes->getRight()); + + // test fast recover + + $dql = 'UPDATE '.RootCategory::class.' node'; + $dql .= ' SET node.lft = 1'; + $dql .= ' WHERE node.id = 8'; + $this->em->createQuery($dql)->execute(); + + $this->em->clear(); // must clear cached entities + + static::assertGreaterThan(0, $repo->verify()); + + $repo->recoverFast([ + 'sortByField' => 'title', + 'sortDirection' => 'ASC', + ]); + $this->em->clear(); // must clear cached entities + + static::assertTrue($repo->verify()); } - /** - * @test - */ - public function shouldRemoveTreeLeafFromTree() + public function testShouldRemoveTreeLeafFromTree(): void { $this->populateMore(); - $repo = $this->em->getRepository(self::CATEGORY); - $onions = $repo->findOneByTitle('Onions'); + $repo = $this->em->getRepository(RootCategory::class); + $onions = $repo->findOneBy(['title' => 'Onions']); $id = $onions->getId(); $repo->removeFromTree($onions); - $this->assertNull($repo->find($id)); + static::assertNull($repo->find($id)); $this->em->clear(); - $vegies = $repo->findOneByTitle('Vegitables'); - $this->assertTrue($repo->verify()); + static::assertTrue($repo->verify()); } - /** - * @test - */ - public function getRootNodesTest() + public function testGetRootNodesTest(): void { - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); // Test getRootNodes without custom ordering $roots = $repo->getRootNodes(); - $this->assertEquals(2, count($roots)); - $this->assertEquals('Food', $roots[0]->getTitle()); - $this->assertEquals('Sports', $roots[1]->getTitle()); + static::assertCount(2, $roots); + static::assertSame('Food', $roots[0]->getTitle()); + static::assertSame('Sports', $roots[1]->getTitle()); // Test getRootNodes with custom ordering $roots = $repo->getRootNodes('title', 'desc'); - $this->assertEquals(2, count($roots)); - $this->assertEquals('Sports', $roots[0]->getTitle()); - $this->assertEquals('Food', $roots[1]->getTitle()); + static::assertCount(2, $roots); + static::assertSame('Sports', $roots[0]->getTitle()); + static::assertSame('Food', $roots[1]->getTitle()); } - /** - * @test - */ - public function changeChildrenIndexTest() + public function testChangeChildrenIndexTest(): void { - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); $childrenIndex = 'myChildren'; $repo->setChildrenIndex($childrenIndex); $tree = $repo->childrenHierarchy(); - $this->assertInternalType('array', $tree[0][$childrenIndex]); + static::assertIsArray($tree[0][$childrenIndex]); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CATEGORY, - ); + return [ + RootCategory::class, + ]; } - private function populateMore() + private function populateMore(): void { - $vegies = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Vegitables'); + $vegies = $this->em->getRepository(RootCategory::class) + ->findOneBy(['title' => 'Vegitables']); $cabbages = new RootCategory(); $cabbages->setParent($vegies); @@ -465,28 +602,28 @@ private function populateMore() $this->em->flush(); } - private function populate() + private function populate(): void { $root = new RootCategory(); - $root->setTitle("Food"); + $root->setTitle('Food'); $root2 = new RootCategory(); - $root2->setTitle("Sports"); + $root2->setTitle('Sports'); $child = new RootCategory(); - $child->setTitle("Fruits"); + $child->setTitle('Fruits'); $child->setParent($root); $child2 = new RootCategory(); - $child2->setTitle("Vegitables"); + $child2->setTitle('Vegitables'); $child2->setParent($root); $childsChild = new RootCategory(); - $childsChild->setTitle("Carrots"); + $childsChild->setTitle('Carrots'); $childsChild->setParent($child2); $potatoes = new RootCategory(); - $potatoes->setTitle("Potatoes"); + $potatoes->setTitle('Potatoes'); $potatoes->setParent($child2); $this->em->persist($root); diff --git a/tests/Gedmo/Tree/NestedTreeRootTest.php b/tests/Gedmo/Tree/NestedTreeRootTest.php index 73a8844251..e53f7fa00f 100644 --- a/tests/Gedmo/Tree/NestedTreeRootTest.php +++ b/tests/Gedmo/Tree/NestedTreeRootTest.php @@ -1,59 +1,64 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\RootCategory; +use Doctrine\ORM\OptimisticLockException; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\ForeignRootCategory; +use Gedmo\Tests\Tree\Fixture\RootCategory; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class NestedTreeRootTest extends BaseTestCaseORM +final class NestedTreeRootTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\RootCategory"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - /** - * @test - */ - public function shouldRemoveAndSynchronize() + public function testShouldRemoveAndSynchronize(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $vegies = $repo->findOneByTitle('Vegitables'); + $repo = $this->em->getRepository(RootCategory::class); + $vegies = $repo->findOneBy(['title' => 'Vegitables']); $this->em->remove($vegies); $this->em->flush(); - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); - $this->assertEquals(1, $food->getLeft()); - $this->assertEquals(4, $food->getRight()); + static::assertSame(1, $food->getLeft()); + static::assertSame(4, $food->getRight()); $vegies = new RootCategory(); $vegies->setTitle('Vegies'); $repo->persistAsFirstChildOf($vegies, $food); $this->em->flush(); - $this->assertEquals(1, $food->getLeft()); - $this->assertEquals(6, $food->getRight()); + static::assertSame(1, $food->getLeft()); + static::assertSame(6, $food->getRight()); - $this->assertEquals(2, $vegies->getLeft()); - $this->assertEquals(3, $vegies->getRight()); + static::assertSame(2, $vegies->getLeft()); + static::assertSame(3, $vegies->getRight()); } /*public function testHeavyLoad() @@ -64,7 +69,7 @@ public function shouldRemoveAndSynchronize() $minutes = intval($took / 60); $seconds = $took % 60; echo sprintf("%s --> %02d:%02d", $msg, $minutes, $seconds) . PHP_EOL; }; - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); $parent = null; $num = 800; for($i = 0; $i < 500; $i++) { @@ -87,179 +92,177 @@ public function shouldRemoveAndSynchronize() $dumpTime($start, $num.' - inserts took:'); $start = microtime(true); // test moving - $target = $repo->findOneByTitle('cat300'); - $dest = $repo->findOneByTitle('cat2000'); + $target = $repo->findOneBy(['title' => 'cat300']); + $dest = $repo->findOneBy(['title' => 'cat2000']); $target->setParent($dest); - $target2 = $repo->findOneByTitle('cat450'); - $dest2 = $repo->findOneByTitle('cat2500'); + $target2 = $repo->findOneBy(['title' => 'cat450']); + $dest2 = $repo->findOneBy(['title' => 'cat2500']); $target2->setParent($dest2); $this->em->flush(); $dumpTime($start, 'moving took:'); }*/ - public function testTheTree() + public function testTheTree(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $node = $repo->findOneByTitle('Food'); + $repo = $this->em->getRepository(RootCategory::class); + $node = $repo->findOneBy(['title' => 'Food']); - $this->assertEquals(1, $node->getRoot()); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(0, $node->getLevel()); - $this->assertEquals(10, $node->getRight()); + static::assertSame(1, $node->getRoot()); + static::assertSame(1, $node->getLeft()); + static::assertSame(1, $node->getLevel()); + static::assertSame(10, $node->getRight()); - $node = $repo->findOneByTitle('Sports'); + $node = $repo->findOneBy(['title' => 'Sports']); - $this->assertEquals(2, $node->getRoot()); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(0, $node->getLevel()); - $this->assertEquals(2, $node->getRight()); + static::assertSame(2, $node->getRoot()); + static::assertSame(1, $node->getLeft()); + static::assertSame(1, $node->getLevel()); + static::assertSame(2, $node->getRight()); - $node = $repo->findOneByTitle('Fruits'); + $node = $repo->findOneBy(['title' => 'Fruits']); - $this->assertEquals(1, $node->getRoot()); - $this->assertEquals(2, $node->getLeft()); - $this->assertEquals(1, $node->getLevel()); - $this->assertEquals(3, $node->getRight()); + static::assertSame(1, $node->getRoot()); + static::assertSame(2, $node->getLeft()); + static::assertSame(2, $node->getLevel()); + static::assertSame(3, $node->getRight()); - $node = $repo->findOneByTitle('Vegitables'); + $node = $repo->findOneBy(['title' => 'Vegitables']); - $this->assertEquals(1, $node->getRoot()); - $this->assertEquals(4, $node->getLeft()); - $this->assertEquals(1, $node->getLevel()); - $this->assertEquals(9, $node->getRight()); + static::assertSame(1, $node->getRoot()); + static::assertSame(4, $node->getLeft()); + static::assertSame(2, $node->getLevel()); + static::assertSame(9, $node->getRight()); - $node = $repo->findOneByTitle('Carrots'); + $node = $repo->findOneBy(['title' => 'Carrots']); - $this->assertEquals(1, $node->getRoot()); - $this->assertEquals(5, $node->getLeft()); - $this->assertEquals(2, $node->getLevel()); - $this->assertEquals(6, $node->getRight()); + static::assertSame(1, $node->getRoot()); + static::assertSame(5, $node->getLeft()); + static::assertSame(3, $node->getLevel()); + static::assertSame(6, $node->getRight()); - $node = $repo->findOneByTitle('Potatoes'); + $node = $repo->findOneBy(['title' => 'Potatoes']); - $this->assertEquals(1, $node->getRoot()); - $this->assertEquals(7, $node->getLeft()); - $this->assertEquals(2, $node->getLevel()); - $this->assertEquals(8, $node->getRight()); + static::assertSame(1, $node->getRoot()); + static::assertSame(7, $node->getLeft()); + static::assertSame(3, $node->getLevel()); + static::assertSame(8, $node->getRight()); } - public function testSetParentToNull() + public function testSetParentToNull(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $node = $repo->findOneByTitle('Vegitables'); + $repo = $this->em->getRepository(RootCategory::class); + $node = $repo->findOneBy(['title' => 'Vegitables']); $node->setParent(null); $this->em->persist($node); $this->em->flush(); $this->em->clear(); - $node = $repo->findOneByTitle('Vegitables'); - $this->assertEquals(4, $node->getRoot()); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(6, $node->getRight()); - $this->assertEquals(0, $node->getLevel()); + $node = $repo->findOneBy(['title' => 'Vegitables']); + static::assertSame(4, $node->getRoot()); + static::assertSame(1, $node->getLeft()); + static::assertSame(6, $node->getRight()); + static::assertSame(1, $node->getLevel()); } - public function testTreeUpdateShiftToNextBranch() + public function testTreeUpdateShiftToNextBranch(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $sport = $repo->findOneByTitle('Sports'); - $food = $repo->findOneByTitle('Food'); + $repo = $this->em->getRepository(RootCategory::class); + $sport = $repo->findOneBy(['title' => 'Sports']); + $food = $repo->findOneBy(['title' => 'Food']); $sport->setParent($food); $this->em->persist($sport); $this->em->flush(); $this->em->clear(); - $node = $repo->findOneByTitle('Food'); + $node = $repo->findOneBy(['title' => 'Food']); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(12, $node->getRight()); + static::assertSame(1, $node->getLeft()); + static::assertSame(12, $node->getRight()); - $node = $repo->findOneByTitle('Sports'); + $node = $repo->findOneBy(['title' => 'Sports']); - $this->assertEquals(1, $node->getRoot()); - $this->assertEquals(2, $node->getLeft()); - $this->assertEquals(1, $node->getLevel()); - $this->assertEquals(3, $node->getRight()); + static::assertSame(1, $node->getRoot()); + static::assertSame(2, $node->getLeft()); + static::assertSame(2, $node->getLevel()); + static::assertSame(3, $node->getRight()); - $node = $repo->findOneByTitle('Vegitables'); + $node = $repo->findOneBy(['title' => 'Vegitables']); - $this->assertEquals(6, $node->getLeft()); - $this->assertEquals(11, $node->getRight()); + static::assertSame(6, $node->getLeft()); + static::assertSame(11, $node->getRight()); } - public function testTreeUpdateShiftToRoot() + public function testTreeUpdateShiftToRoot(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $vegies = $repo->findOneByTitle('Vegitables'); + $repo = $this->em->getRepository(RootCategory::class); + $vegies = $repo->findOneBy(['title' => 'Vegitables']); $vegies->setParent(null); $this->em->persist($vegies); $this->em->flush(); $this->em->clear(); - $node = $repo->findOneByTitle('Food'); + $node = $repo->findOneBy(['title' => 'Food']); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(4, $node->getRight()); + static::assertSame(1, $node->getLeft()); + static::assertSame(4, $node->getRight()); - $node = $repo->findOneByTitle('Vegitables'); + $node = $repo->findOneBy(['title' => 'Vegitables']); - $this->assertEquals(4, $node->getRoot()); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(0, $node->getLevel()); - $this->assertEquals(6, $node->getRight()); + static::assertSame(4, $node->getRoot()); + static::assertSame(1, $node->getLeft()); + static::assertSame(1, $node->getLevel()); + static::assertSame(6, $node->getRight()); - $node = $repo->findOneByTitle('Potatoes'); + $node = $repo->findOneBy(['title' => 'Potatoes']); - $this->assertEquals(4, $node->getRoot()); - $this->assertEquals(4, $node->getLeft()); - $this->assertEquals(1, $node->getLevel()); - $this->assertEquals(5, $node->getRight()); + static::assertSame(4, $node->getRoot()); + static::assertSame(4, $node->getLeft()); + static::assertSame(2, $node->getLevel()); + static::assertSame(5, $node->getRight()); } - public function testTreeUpdateShiftToOtherParent() + public function testTreeUpdateShiftToOtherParent(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $carrots = $repo->findOneByTitle('Carrots'); - $food = $repo->findOneByTitle('Food'); + $repo = $this->em->getRepository(RootCategory::class); + $carrots = $repo->findOneBy(['title' => 'Carrots']); + $food = $repo->findOneBy(['title' => 'Food']); $carrots->setParent($food); $this->em->persist($carrots); $this->em->flush(); $this->em->clear(); - $node = $repo->findOneByTitle('Food'); + $node = $repo->findOneBy(['title' => 'Food']); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(10, $node->getRight()); + static::assertSame(1, $node->getLeft()); + static::assertSame(10, $node->getRight()); - $node = $repo->findOneByTitle('Carrots'); + $node = $repo->findOneBy(['title' => 'Carrots']); - $this->assertEquals(1, $node->getRoot()); - $this->assertEquals(2, $node->getLeft()); - $this->assertEquals(1, $node->getLevel()); - $this->assertEquals(3, $node->getRight()); + static::assertSame(1, $node->getRoot()); + static::assertSame(2, $node->getLeft()); + static::assertSame(2, $node->getLevel()); + static::assertSame(3, $node->getRight()); - $node = $repo->findOneByTitle('Potatoes'); + $node = $repo->findOneBy(['title' => 'Potatoes']); - $this->assertEquals(1, $node->getRoot()); - $this->assertEquals(7, $node->getLeft()); - $this->assertEquals(2, $node->getLevel()); - $this->assertEquals(8, $node->getRight()); + static::assertSame(1, $node->getRoot()); + static::assertSame(7, $node->getLeft()); + static::assertSame(3, $node->getLevel()); + static::assertSame(8, $node->getRight()); } - /** - * @expectedException UnexpectedValueException - */ - public function testTreeUpdateShiftToChildParent() + public function testTreeUpdateShiftToChildParent(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $vegies = $repo->findOneByTitle('Vegitables'); - $food = $repo->findOneByTitle('Food'); + $this->expectException('UnexpectedValueException'); + $repo = $this->em->getRepository(RootCategory::class); + $vegies = $repo->findOneBy(['title' => 'Vegitables']); + $food = $repo->findOneBy(['title' => 'Food']); $food->setParent($vegies); $this->em->persist($food); @@ -267,15 +270,15 @@ public function testTreeUpdateShiftToChildParent() $this->em->clear(); } - public function testTwoUpdateOperations() + public function testTwoUpdateOperations(): void { - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(RootCategory::class); - $sport = $repo->findOneByTitle('Sports'); - $food = $repo->findOneByTitle('Food'); + $sport = $repo->findOneBy(['title' => 'Sports']); + $food = $repo->findOneBy(['title' => 'Food']); $sport->setParent($food); - $vegies = $repo->findOneByTitle('Vegitables'); + $vegies = $repo->findOneBy(['title' => 'Vegitables']); $vegies->setParent(null); $this->em->persist($vegies); @@ -283,72 +286,243 @@ public function testTwoUpdateOperations() $this->em->flush(); $this->em->clear(); - $node = $repo->findOneByTitle('Carrots'); + $node = $repo->findOneBy(['title' => 'Carrots']); - $this->assertEquals(4, $node->getRoot()); - $this->assertEquals(2, $node->getLeft()); - $this->assertEquals(1, $node->getLevel()); - $this->assertEquals(3, $node->getRight()); + static::assertSame(4, $node->getRoot()); + static::assertSame(2, $node->getLeft()); + static::assertSame(2, $node->getLevel()); + static::assertSame(3, $node->getRight()); - $node = $repo->findOneByTitle('Vegitables'); + $node = $repo->findOneBy(['title' => 'Vegitables']); - $this->assertEquals(4, $node->getRoot()); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(0, $node->getLevel()); - $this->assertEquals(6, $node->getRight()); + static::assertSame(4, $node->getRoot()); + static::assertSame(1, $node->getLeft()); + static::assertSame(1, $node->getLevel()); + static::assertSame(6, $node->getRight()); - $node = $repo->findOneByTitle('Sports'); + $node = $repo->findOneBy(['title' => 'Sports']); - $this->assertEquals(1, $node->getRoot()); - $this->assertEquals(2, $node->getLeft()); - $this->assertEquals(1, $node->getLevel()); - $this->assertEquals(3, $node->getRight()); + static::assertSame(1, $node->getRoot()); + static::assertSame(2, $node->getLeft()); + static::assertSame(2, $node->getLevel()); + static::assertSame(3, $node->getRight()); } - public function testRemoval() + public function testRemoval(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $vegies = $repo->findOneByTitle('Vegitables'); + $repo = $this->em->getRepository(RootCategory::class); + $vegies = $repo->findOneBy(['title' => 'Vegitables']); $this->em->remove($vegies); $this->em->flush(); $this->em->clear(); - $node = $repo->findOneByTitle('Food'); + $node = $repo->findOneBy(['title' => 'Food']); + + static::assertSame(1, $node->getLeft()); + static::assertSame(4, $node->getRight()); + } + + /** + * @throws OptimisticLockException + */ + public function testTreeWithRootPointingAtAnotherTable(): void + { + // depopulate, i don't want the other stuff in db + $repo = $this->em->getRepository(ForeignRootCategory::class); + $all = $repo->findAll(); + foreach ($all as $one) { + $this->em->remove($one); + } + $this->em->flush(); - $this->assertEquals(1, $node->getLeft()); - $this->assertEquals(4, $node->getRight()); + $fiction = new ForeignRootCategory(); + $fiction->setTitle('Fiction Books'); + $fiction->setRoot(1); // Lets pretend this points to another table, and root id 1 is "Books" + + $fact = new ForeignRootCategory(); + $fact->setTitle('Fact Books'); + $fact->setRoot(1); + + $action = new ForeignRootCategory(); + $action->setTitle('Action'); + $action->setRoot(2); // Lets pretend this points to another table, and root id 2 is "Movies" + + $comedy = new ForeignRootCategory(); + $comedy->setTitle('Comedy'); + $comedy->setRoot(2); + + $horror = new ForeignRootCategory(); + $horror->setTitle('Horror'); + $horror->setRoot(2); + + // Child categories now + $lotr = new ForeignRootCategory(); + $lotr->setTitle('Lord of the Rings'); + $lotr->setParent($fiction); + $lotr->setRoot(1); + + $warlock = new ForeignRootCategory(); + $warlock->setTitle('The Warlock of Firetop Mountain'); + $warlock->setParent($fiction); + $warlock->setRoot(1); + + $php = new ForeignRootCategory(); + $php->setTitle('PHP open source development'); + $php->setParent($fact); + $php->setRoot(1); + + $dracula = new ForeignRootCategory(); + $dracula->setTitle('Hammer Horror Dracula'); + $dracula->setParent($horror); + $dracula->setRoot(2); + + $frankenstein = new ForeignRootCategory(); + $frankenstein->setTitle('Hammer Horror Frankenstein'); + $frankenstein->setParent($horror); + $frankenstein->setRoot(2); + + $this->em->persist($fact); + $this->em->persist($fiction); + $this->em->persist($comedy); + $this->em->persist($horror); + $this->em->persist($action); + $this->em->persist($lotr); + $this->em->persist($warlock); + $this->em->persist($php); + $this->em->persist($dracula); + $this->em->persist($frankenstein); + $this->em->flush(); + + static::assertSame(1, $fact->getLeft()); + static::assertSame(4, $fact->getRight()); + static::assertSame(0, $fact->getLevel()); + static::assertSame(1, $fact->getRoot()); + static::assertNull($fact->getParent()); + + static::assertSame(5, $fiction->getLeft()); + static::assertSame(10, $fiction->getRight()); + static::assertSame(0, $fiction->getLevel()); + static::assertSame(1, $fiction->getRoot()); + static::assertNull($fiction->getParent()); + + static::assertSame(6, $lotr->getLeft()); + static::assertSame(7, $lotr->getRight()); + static::assertSame(1, $lotr->getLevel()); + static::assertSame(1, $lotr->getRoot()); + static::assertSame($fiction, $lotr->getParent()); + + static::assertSame(8, $warlock->getLeft()); + static::assertSame(9, $warlock->getRight()); + static::assertSame(1, $warlock->getLevel()); + static::assertSame(1, $warlock->getRoot()); + static::assertSame($fiction, $warlock->getParent()); + + static::assertSame(2, $php->getLeft()); + static::assertSame(3, $php->getRight()); + static::assertSame(1, $php->getLevel()); + static::assertSame(1, $php->getRoot()); + static::assertSame($fact, $php->getParent()); + + static::assertSame(1, $comedy->getLeft()); + static::assertSame(2, $comedy->getRight()); + static::assertSame(0, $comedy->getLevel()); + static::assertSame(2, $comedy->getRoot()); + static::assertNull($comedy->getParent()); + + static::assertSame(3, $horror->getLeft()); + static::assertSame(8, $horror->getRight()); + static::assertSame(0, $horror->getLevel()); + static::assertSame(2, $horror->getRoot()); + static::assertNull($horror->getParent()); + + static::assertSame(9, $action->getLeft()); + static::assertSame(10, $action->getRight()); + static::assertSame(0, $action->getLevel()); + static::assertSame(2, $action->getRoot()); + static::assertNull($action->getParent()); + + static::assertSame(4, $dracula->getLeft()); + static::assertSame(5, $dracula->getRight()); + static::assertSame(1, $dracula->getLevel()); + static::assertSame(2, $dracula->getRoot()); + static::assertSame($horror, $dracula->getParent()); + + static::assertSame(6, $frankenstein->getLeft()); + static::assertSame(7, $frankenstein->getRight()); + static::assertSame(1, $frankenstein->getLevel()); + static::assertSame(2, $frankenstein->getRoot()); + static::assertSame($horror, $frankenstein->getParent()); + + // Now move the action movie category up + $repo->moveUp($action); + + static::assertSame(1, $comedy->getLeft()); + static::assertSame(2, $comedy->getRight()); + static::assertSame(0, $comedy->getLevel()); + static::assertSame(2, $comedy->getRoot()); + static::assertNull($comedy->getParent()); + + static::assertSame(3, $action->getLeft()); + static::assertSame(4, $action->getRight()); + static::assertSame(0, $action->getLevel()); + static::assertSame(2, $action->getRoot()); + static::assertNull($action->getParent()); + + static::assertSame(5, $horror->getLeft()); + static::assertSame(10, $horror->getRight()); + static::assertSame(0, $horror->getLevel()); + static::assertSame(2, $horror->getRoot()); + static::assertNull($horror->getParent()); + + static::assertSame(6, $dracula->getLeft()); + static::assertSame(7, $dracula->getRight()); + static::assertSame(1, $dracula->getLevel()); + static::assertSame(2, $dracula->getRoot()); + static::assertSame($horror, $dracula->getParent()); + + static::assertSame(8, $frankenstein->getLeft()); + static::assertSame(9, $frankenstein->getRight()); + static::assertSame(1, $frankenstein->getLevel()); + static::assertSame(2, $frankenstein->getRoot()); + static::assertSame($horror, $frankenstein->getParent()); + + $this->em->clear(); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CATEGORY, - ); + return [ + RootCategory::class, + ForeignRootCategory::class, + ]; } - private function populate() + /** + * @throws OptimisticLockException + */ + private function populate(): void { $root = new RootCategory(); - $root->setTitle("Food"); + $root->setTitle('Food'); $root2 = new RootCategory(); - $root2->setTitle("Sports"); + $root2->setTitle('Sports'); $child = new RootCategory(); - $child->setTitle("Fruits"); + $child->setTitle('Fruits'); $child->setParent($root); $child2 = new RootCategory(); - $child2->setTitle("Vegitables"); + $child2->setTitle('Vegitables'); $child2->setParent($root); $childsChild = new RootCategory(); - $childsChild->setTitle("Carrots"); + $childsChild->setTitle('Carrots'); $childsChild->setParent($child2); $potatoes = new RootCategory(); - $potatoes->setTitle("Potatoes"); + $potatoes->setTitle('Potatoes'); $potatoes->setParent($child2); $this->em->persist($root); diff --git a/tests/Gedmo/Tree/RepositoryTest.php b/tests/Gedmo/Tree/RepositoryTest.php index c03e0ebf40..cd7f6629e7 100644 --- a/tests/Gedmo/Tree/RepositoryTest.php +++ b/tests/Gedmo/Tree/RepositoryTest.php @@ -1,136 +1,187 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\Category; -use Tree\Fixture\CategoryUuid; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Category; +use Gedmo\Tests\Tree\Fixture\CategoryUuid; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class RepositoryTest extends BaseTestCaseORM +final class RepositoryTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\Category"; - const CATEGORY_UUID = "Tree\\Fixture\\CategoryUuid"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testBasicFunctions() + public function testBasicFunctions(): void { - $vegies = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Vegitables'); + $vegies = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Vegitables']); - $food = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Food'); + $food = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Food']); // test childCount - $childCount = $this->em->getRepository(self::CATEGORY) + $childCount = $this->em->getRepository(Category::class) ->childCount($vegies); - $this->assertEquals(2, $childCount); + static::assertSame(2, $childCount); - $childCount = $this->em->getRepository(self::CATEGORY) + $childCount = $this->em->getRepository(Category::class) ->childCount($food); - $this->assertEquals(4, $childCount); + static::assertSame(4, $childCount); - $childCount = $this->em->getRepository(self::CATEGORY) + $childCount = $this->em->getRepository(Category::class) ->childCount($food, true); - $this->assertEquals(2, $childCount); + static::assertSame(2, $childCount); - $childCount = $this->em->getRepository(self::CATEGORY) + $childCount = $this->em->getRepository(Category::class) ->childCount(); - $this->assertEquals(6, $childCount); + static::assertSame(6, $childCount); // test children - $children = $this->em->getRepository(self::CATEGORY) + $children = $this->em->getRepository(Category::class) ->children($vegies); - $this->assertCount(2, $children); - $this->assertEquals('Carrots', $children[0]->getTitle()); - $this->assertEquals('Potatoes', $children[1]->getTitle()); + static::assertCount(2, $children); + static::assertSame('Carrots', $children[0]->getTitle()); + static::assertSame('Potatoes', $children[1]->getTitle()); - $children = $this->em->getRepository(self::CATEGORY) + $children = $this->em->getRepository(Category::class) ->children($food); - $this->assertCount(4, $children); - $this->assertEquals('Fruits', $children[0]->getTitle()); - $this->assertEquals('Vegitables', $children[1]->getTitle()); - $this->assertEquals('Carrots', $children[2]->getTitle()); - $this->assertEquals('Potatoes', $children[3]->getTitle()); + static::assertCount(4, $children); + static::assertSame('Fruits', $children[0]->getTitle()); + static::assertSame('Vegitables', $children[1]->getTitle()); + static::assertSame('Carrots', $children[2]->getTitle()); + static::assertSame('Potatoes', $children[3]->getTitle()); - $children = $this->em->getRepository(self::CATEGORY) + $children = $this->em->getRepository(Category::class) ->children($food, true); - $this->assertCount(2, $children); - $this->assertEquals('Fruits', $children[0]->getTitle()); - $this->assertEquals('Vegitables', $children[1]->getTitle()); + static::assertCount(2, $children); + static::assertSame('Fruits', $children[0]->getTitle()); + static::assertSame('Vegitables', $children[1]->getTitle()); - $children = $this->em->getRepository(self::CATEGORY) + $children = $this->em->getRepository(Category::class) ->children(); - $this->assertCount(6, $children); + static::assertCount(6, $children); + + // test children sorting + + $children = $this->em->getRepository(Category::class) + ->children($food, true, ['title'], 'ASC'); + + static::assertCount(2, $children); + static::assertSame('Fruits', $children[0]->getTitle()); + static::assertSame('Vegitables', $children[1]->getTitle()); + + $children = $this->em->getRepository(Category::class) + ->children($food, false, ['level', 'title'], ['ASC', 'DESC']); + + static::assertCount(4, $children); + static::assertSame('Vegitables', $children[0]->getTitle()); + static::assertSame('Fruits', $children[1]->getTitle()); + static::assertSame('Potatoes', $children[2]->getTitle()); + static::assertSame('Carrots', $children[3]->getTitle()); + + $children = $this->em->getRepository(Category::class) + ->children($food, false, ['level', 'title'], ['ASC']); + + static::assertCount(4, $children); + static::assertSame('Fruits', $children[0]->getTitle()); + static::assertSame('Vegitables', $children[1]->getTitle()); + static::assertSame('Carrots', $children[2]->getTitle()); + static::assertSame('Potatoes', $children[3]->getTitle()); + + // test sorting by single-valued association field + $children = $this->em->getRepository(Category::class) + ->children($food, false, 'parentId'); + + static::assertCount(4, $children); + static::assertSame('Fruits', $children[0]->getTitle()); + static::assertSame('Vegitables', $children[1]->getTitle()); + static::assertSame('Carrots', $children[2]->getTitle()); + static::assertSame('Potatoes', $children[3]->getTitle()); + + $children = $this->em->getRepository(Category::class) + ->children($food, false, ['parentId'], ['ASC']); + + static::assertCount(4, $children); + static::assertSame('Fruits', $children[0]->getTitle()); + static::assertSame('Vegitables', $children[1]->getTitle()); + static::assertSame('Carrots', $children[2]->getTitle()); + static::assertSame('Potatoes', $children[3]->getTitle()); // path - $path = $this->em->getRepository(self::CATEGORY) + $path = $this->em->getRepository(Category::class) ->getPath($vegies); - $this->assertCount(2, $path); - $this->assertEquals('Food', $path[0]->getTitle()); - $this->assertEquals('Vegitables', $path[1]->getTitle()); + static::assertCount(2, $path); + static::assertSame('Food', $path[0]->getTitle()); + static::assertSame('Vegitables', $path[1]->getTitle()); - $carrots = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Carrots'); + $carrots = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Carrots']); - $path = $this->em->getRepository(self::CATEGORY) + $path = $this->em->getRepository(Category::class) ->getPath($carrots); - $this->assertCount(3, $path); - $this->assertEquals('Food', $path[0]->getTitle()); - $this->assertEquals('Vegitables', $path[1]->getTitle()); - $this->assertEquals('Carrots', $path[2]->getTitle()); + static::assertCount(3, $path); + static::assertSame('Food', $path[0]->getTitle()); + static::assertSame('Vegitables', $path[1]->getTitle()); + static::assertSame('Carrots', $path[2]->getTitle()); // leafs - $leafs = $this->em->getRepository(self::CATEGORY) + $leafs = $this->em->getRepository(Category::class) ->getLeafs(); - $this->assertCount(4, $leafs); - $this->assertEquals('Fruits', $leafs[0]->getTitle()); - $this->assertEquals('Carrots', $leafs[1]->getTitle()); - $this->assertEquals('Potatoes', $leafs[2]->getTitle()); - $this->assertEquals('Sports', $leafs[3]->getTitle()); + static::assertCount(4, $leafs); + static::assertSame('Fruits', $leafs[0]->getTitle()); + static::assertSame('Carrots', $leafs[1]->getTitle()); + static::assertSame('Potatoes', $leafs[2]->getTitle()); + static::assertSame('Sports', $leafs[3]->getTitle()); } - public function testAdvancedFunctions() + public function testAdvancedFunctions(): void { $this->populateMore(); - $onions = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Onions'); - $repo = $this->em->getRepository(self::CATEGORY); - $meta = $this->em->getClassMetadata(self::CATEGORY); + $onions = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Onions']); + $repo = $this->em->getRepository(Category::class); + $meta = $this->em->getClassMetadata(Category::class); $left = $meta->getReflectionProperty('lft')->getValue($onions); $right = $meta->getReflectionProperty('rgt')->getValue($onions); - $this->assertEquals(11, $left); - $this->assertEquals(12, $right); + static::assertSame(11, $left); + static::assertSame(12, $right); // move up onions by one position @@ -139,8 +190,8 @@ public function testAdvancedFunctions() $left = $meta->getReflectionProperty('lft')->getValue($onions); $right = $meta->getReflectionProperty('rgt')->getValue($onions); - $this->assertEquals(9, $left); - $this->assertEquals(10, $right); + static::assertSame(9, $left); + static::assertSame(10, $right); // move down onions by one position $repo->moveDown($onions, 1); @@ -148,8 +199,8 @@ public function testAdvancedFunctions() $left = $meta->getReflectionProperty('lft')->getValue($onions); $right = $meta->getReflectionProperty('rgt')->getValue($onions); - $this->assertEquals(11, $left); - $this->assertEquals(12, $right); + static::assertSame(11, $left); + static::assertSame(12, $right); // move to the up onions on this level @@ -158,123 +209,122 @@ public function testAdvancedFunctions() $left = $meta->getReflectionProperty('lft')->getValue($onions); $right = $meta->getReflectionProperty('rgt')->getValue($onions); - $this->assertEquals(5, $left); - $this->assertEquals(6, $right); + static::assertSame(5, $left); + static::assertSame(6, $right); // test tree reordering // reorder tree by title - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $repo->reorder($food, 'title'); - $node = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Cabbages'); + $node = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Cabbages']); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(5, $left); - $this->assertEquals(6, $right); + static::assertSame(5, $left); + static::assertSame(6, $right); - $node = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Carrots'); + $node = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Carrots']); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(7, $left); - $this->assertEquals(8, $right); + static::assertSame(7, $left); + static::assertSame(8, $right); - $node = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Onions'); + $node = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Onions']); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(9, $left); - $this->assertEquals(10, $right); + static::assertSame(9, $left); + static::assertSame(10, $right); - $node = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Potatoes'); + $node = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Potatoes']); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(11, $left); - $this->assertEquals(12, $right); + static::assertSame(11, $left); + static::assertSame(12, $right); // test removal with reparenting - $vegies = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Vegitables'); + $vegies = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Vegitables']); $repo->removeFromTree($vegies); $this->em->clear(); // clear all cached nodes - $vegies = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Vegitables'); + $vegies = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Vegitables']); - $this->assertNull($vegies); + static::assertNull($vegies); - $node = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Fruits'); + $node = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Fruits']); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(2, $left); - $this->assertEquals(3, $right); - $this->assertEquals('Food', $node->getParent()->getTitle()); + static::assertSame(2, $left); + static::assertSame(3, $right); + static::assertSame('Food', $node->getParent()->getTitle()); - $node = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Cabbages'); + $node = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Cabbages']); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(4, $left); - $this->assertEquals(5, $right); - $this->assertEquals('Food', $node->getParent()->getTitle()); + static::assertSame(4, $left); + static::assertSame(5, $right); + static::assertSame('Food', $node->getParent()->getTitle()); } - public function testRootRemoval() + public function testRootRemoval(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $meta = $this->em->getClassMetadata(self::CATEGORY); + $repo = $this->em->getRepository(Category::class); + $meta = $this->em->getClassMetadata(Category::class); $this->populateMore(); - $food = $repo->findOneByTitle('Food'); + $food = $repo->findOneBy(['title' => 'Food']); $repo->removeFromTree($food); $this->em->clear(); - $food = $repo->findOneByTitle('Food'); - $this->assertNull($food); + $food = $repo->findOneBy(['title' => 'Food']); + static::assertNull($food); - $node = $repo->findOneByTitle('Fruits'); + $node = $repo->findOneBy(['title' => 'Fruits']); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(1, $left); - $this->assertEquals(2, $right); - $this->assertNull($node->getParent()); + static::assertSame(1, $left); + static::assertSame(2, $right); + static::assertNull($node->getParent()); - $node = $repo->findOneByTitle('Vegitables'); + $node = $repo->findOneBy(['title' => 'Vegitables']); $left = $meta->getReflectionProperty('lft')->getValue($node); $right = $meta->getReflectionProperty('rgt')->getValue($node); - $this->assertEquals(3, $left); - $this->assertEquals(12, $right); - $this->assertNull($node->getParent()); + static::assertSame(3, $left); + static::assertSame(12, $right); + static::assertNull($node->getParent()); } - public function testVerificationAndRecover() + public function testVerificationAndRecover(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $meta = $this->em->getClassMetadata(self::CATEGORY); + $repo = $this->em->getRepository(Category::class); $this->populateMore(); // test verification of tree - $this->assertTrue($repo->verify()); + static::assertTrue($repo->verify()); // now lets brake something - $dql = 'UPDATE '.self::CATEGORY.' node'; - $dql .= ' SET node.lft = 1'; + $dql = 'UPDATE '.Category::class.' node'; + $dql .= ' SET node.lft = 1, node.level = 99'; $dql .= ' WHERE node.id = 8'; $q = $this->em->createQuery($dql); $q->getSingleScalarResult(); @@ -284,147 +334,150 @@ public function testVerificationAndRecover() // verify again $result = $repo->verify(); - $this->assertTrue(is_array($result)); + static::assertIsArray($result); - $this->assertArrayHasKey(0, $result); - $this->assertArrayHasKey(1, $result); - $this->assertArrayHasKey(2, $result); + static::assertArrayHasKey(0, $result); + static::assertArrayHasKey(1, $result); + static::assertArrayHasKey(2, $result); + static::assertArrayHasKey(3, $result); $duplicate = $result[0]; $missing = $result[1]; $invalidLeft = $result[2]; + $invalidLevel = $result[3]; - $this->assertEquals('index [1], duplicate', $duplicate); - $this->assertEquals('index [11], missing', $missing); - $this->assertEquals('node [8] left is less than parent`s [4] left value', $invalidLeft); + static::assertSame('index [1], duplicate', $duplicate); + static::assertSame('index [11], missing', $missing); + static::assertSame('node [8] left is less than parent`s [4] left value', $invalidLeft); + static::assertSame('node [8] should be on the level right after its parent`s [4] level', $invalidLevel); // test recover functionality $repo->recover(); $this->em->flush(); - $this->assertTrue($repo->verify()); + static::assertTrue($repo->verify()); } - public function testMoveRootNode() + public function testMoveRootNode(): void { - $repo = $this->em->getRepository(self::CATEGORY); - $food = $repo->findOneByTitle('Food'); + $repo = $this->em->getRepository(Category::class); + $food = $repo->findOneBy(['title' => 'Food']); $repo->moveDown($food, 1); - $meta = $this->em->getClassMetadata(self::CATEGORY); + $meta = $this->em->getClassMetadata(Category::class); $left = $meta->getReflectionProperty('lft')->getValue($food); $right = $meta->getReflectionProperty('rgt')->getValue($food); - $this->assertEquals(3, $left); - $this->assertEquals(12, $right); - $this->assertNull($food->getParent()); + static::assertSame(3, $left); + static::assertSame(12, $right); + static::assertNull($food->getParent()); - $this->assertTrue($repo->verify()); + static::assertTrue($repo->verify()); } - public function testIssue273() + public function testIssue273(): void { $this->populateUuid(); - $vegies = $this->em->getRepository(self::CATEGORY_UUID) - ->findOneByTitle('Vegitables'); + $vegies = $this->em->getRepository(CategoryUuid::class) + ->findOneBy(['title' => 'Vegitables']); - $food = $this->em->getRepository(self::CATEGORY_UUID) - ->findOneByTitle('Food'); + $food = $this->em->getRepository(CategoryUuid::class) + ->findOneBy(['title' => 'Food']); // test childCount - $childCount = $this->em->getRepository(self::CATEGORY_UUID) + $childCount = $this->em->getRepository(CategoryUuid::class) ->childCount($vegies); - $this->assertEquals(2, $childCount); + static::assertSame(2, $childCount); - $childCount = $this->em->getRepository(self::CATEGORY_UUID) + $childCount = $this->em->getRepository(CategoryUuid::class) ->childCount($food); - $this->assertEquals(4, $childCount); + static::assertSame(4, $childCount); - $childCount = $this->em->getRepository(self::CATEGORY_UUID) + $childCount = $this->em->getRepository(CategoryUuid::class) ->childCount($food, true); - $this->assertEquals(2, $childCount); + static::assertSame(2, $childCount); - $childCount = $this->em->getRepository(self::CATEGORY_UUID) + $childCount = $this->em->getRepository(CategoryUuid::class) ->childCount(); - $this->assertEquals(6, $childCount); + static::assertSame(6, $childCount); // test children - $children = $this->em->getRepository(self::CATEGORY_UUID) + $children = $this->em->getRepository(CategoryUuid::class) ->children($vegies); - $this->assertCount(2, $children); - $this->assertEquals('Carrots', $children[0]->getTitle()); - $this->assertEquals('Potatoes', $children[1]->getTitle()); + static::assertCount(2, $children); + static::assertSame('Carrots', $children[0]->getTitle()); + static::assertSame('Potatoes', $children[1]->getTitle()); - $children = $this->em->getRepository(self::CATEGORY_UUID) + $children = $this->em->getRepository(CategoryUuid::class) ->children($food); - $this->assertCount(4, $children); - $this->assertEquals('Fruits', $children[0]->getTitle()); - $this->assertEquals('Vegitables', $children[1]->getTitle()); - $this->assertEquals('Carrots', $children[2]->getTitle()); - $this->assertEquals('Potatoes', $children[3]->getTitle()); + static::assertCount(4, $children); + static::assertSame('Fruits', $children[0]->getTitle()); + static::assertSame('Vegitables', $children[1]->getTitle()); + static::assertSame('Carrots', $children[2]->getTitle()); + static::assertSame('Potatoes', $children[3]->getTitle()); - $children = $this->em->getRepository(self::CATEGORY_UUID) + $children = $this->em->getRepository(CategoryUuid::class) ->children($food, true); - $this->assertCount(2, $children); - $this->assertEquals('Fruits', $children[0]->getTitle()); - $this->assertEquals('Vegitables', $children[1]->getTitle()); + static::assertCount(2, $children); + static::assertSame('Fruits', $children[0]->getTitle()); + static::assertSame('Vegitables', $children[1]->getTitle()); - $children = $this->em->getRepository(self::CATEGORY_UUID) + $children = $this->em->getRepository(CategoryUuid::class) ->children(); - $this->assertCount(6, $children); + static::assertCount(6, $children); // path - $path = $this->em->getRepository(self::CATEGORY_UUID) + $path = $this->em->getRepository(CategoryUuid::class) ->getPath($vegies); - $this->assertCount(2, $path); - $this->assertEquals('Food', $path[0]->getTitle()); - $this->assertEquals('Vegitables', $path[1]->getTitle()); + static::assertCount(2, $path); + static::assertSame('Food', $path[0]->getTitle()); + static::assertSame('Vegitables', $path[1]->getTitle()); - $carrots = $this->em->getRepository(self::CATEGORY_UUID) - ->findOneByTitle('Carrots'); + $carrots = $this->em->getRepository(CategoryUuid::class) + ->findOneBy(['title' => 'Carrots']); - $path = $this->em->getRepository(self::CATEGORY_UUID) + $path = $this->em->getRepository(CategoryUuid::class) ->getPath($carrots); - $this->assertCount(3, $path); - $this->assertEquals('Food', $path[0]->getTitle()); - $this->assertEquals('Vegitables', $path[1]->getTitle()); - $this->assertEquals('Carrots', $path[2]->getTitle()); + static::assertCount(3, $path); + static::assertSame('Food', $path[0]->getTitle()); + static::assertSame('Vegitables', $path[1]->getTitle()); + static::assertSame('Carrots', $path[2]->getTitle()); // leafs - $leafs = $this->em->getRepository(self::CATEGORY_UUID) + $leafs = $this->em->getRepository(CategoryUuid::class) ->getLeafs($path[0]); - $this->assertCount(3, $leafs); - $this->assertEquals('Fruits', $leafs[0]->getTitle()); - $this->assertEquals('Carrots', $leafs[1]->getTitle()); - $this->assertEquals('Potatoes', $leafs[2]->getTitle()); + static::assertCount(3, $leafs); + static::assertSame('Fruits', $leafs[0]->getTitle()); + static::assertSame('Carrots', $leafs[1]->getTitle()); + static::assertSame('Potatoes', $leafs[2]->getTitle()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CATEGORY, - self::CATEGORY_UUID, - ); + return [ + Category::class, + CategoryUuid::class, + ]; } - private function populateMore() + private function populateMore(): void { - $vegies = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Vegitables'); + $vegies = $this->em->getRepository(Category::class) + ->findOneBy(['title' => 'Vegitables']); $cabbages = new Category(); $cabbages->setParent($vegies); @@ -440,28 +493,28 @@ private function populateMore() $this->em->clear(); } - private function populate() + private function populate(): void { $root = new Category(); - $root->setTitle("Food"); + $root->setTitle('Food'); $root2 = new Category(); - $root2->setTitle("Sports"); + $root2->setTitle('Sports'); $child = new Category(); - $child->setTitle("Fruits"); + $child->setTitle('Fruits'); $child->setParent($root); $child2 = new Category(); - $child2->setTitle("Vegitables"); + $child2->setTitle('Vegitables'); $child2->setParent($root); $childsChild = new Category(); - $childsChild->setTitle("Carrots"); + $childsChild->setTitle('Carrots'); $childsChild->setParent($child2); $potatoes = new Category(); - $potatoes->setTitle("Potatoes"); + $potatoes->setTitle('Potatoes'); $potatoes->setParent($child2); $this->em->persist($root); @@ -474,28 +527,28 @@ private function populate() $this->em->clear(); } - private function populateUuid() + private function populateUuid(): void { $root = new CategoryUuid(); - $root->setTitle("Food"); + $root->setTitle('Food'); $root2 = new CategoryUuid(); - $root2->setTitle("Sports"); + $root2->setTitle('Sports'); $child = new CategoryUuid(); - $child->setTitle("Fruits"); + $child->setTitle('Fruits'); $child->setParent($root); $child2 = new CategoryUuid(); - $child2->setTitle("Vegitables"); + $child2->setTitle('Vegitables'); $child2->setParent($root); $childsChild = new CategoryUuid(); - $childsChild->setTitle("Carrots"); + $childsChild->setTitle('Carrots'); $childsChild->setParent($child2); $potatoes = new CategoryUuid(); - $potatoes->setTitle("Potatoes"); + $potatoes->setTitle('Potatoes'); $potatoes->setParent($child2); $this->em->persist($root); diff --git a/tests/Gedmo/Tree/TranslatableSluggableTreeTest.php b/tests/Gedmo/Tree/TranslatableSluggableTreeTest.php index 0b5dc634d3..2aa69859cc 100644 --- a/tests/Gedmo/Tree/TranslatableSluggableTreeTest.php +++ b/tests/Gedmo/Tree/TranslatableSluggableTreeTest.php @@ -1,34 +1,36 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\BehavioralCategory; -use Tree\Fixture\Article; -use Tree\Fixture\Comment; -use Gedmo\Translatable\TranslatableListener; -use Gedmo\Translatable\Entity\Translation; use Gedmo\Sluggable\SluggableListener; -use Doctrine\ORM\Proxy\Proxy; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Article; +use Gedmo\Tests\Tree\Fixture\BehavioralCategory; +use Gedmo\Tests\Tree\Fixture\Comment; +use Gedmo\Translatable\Entity\Translation; +use Gedmo\Translatable\TranslatableListener; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TranslatableSluggableTreeTest extends BaseTestCaseORM +final class TranslatableSluggableTreeTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\BehavioralCategory"; - const ARTICLE = "Tree\\Fixture\\Article"; - const COMMENT = "Tree\\Fixture\\Comment"; - const TRANSLATION = "Gedmo\\Translatable\\Entity\\Translation"; - - private $translatableListener; + private TranslatableListener $translatableListener; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -39,22 +41,22 @@ protected function setUp() $evm->addEventSubscriber(new SluggableListener()); $evm->addEventSubscriber($this->translatableListener); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); $this->populate(); } - public function testNestedBehaviors() + public function testNestedBehaviors(): void { - $vegies = $this->em->getRepository(self::CATEGORY) - ->findOneByTitle('Vegitables'); + $vegies = $this->em->getRepository(BehavioralCategory::class) + ->findOneBy(['title' => 'Vegitables']); - $childCount = $this->em->getRepository(self::CATEGORY) + $childCount = $this->em->getRepository(BehavioralCategory::class) ->childCount($vegies); - $this->assertEquals(2, $childCount); + static::assertSame(2, $childCount); // test slug - $this->assertEquals('vegitables', $vegies->getSlug()); + static::assertSame('vegitables', $vegies->getSlug()); // run second translation test @@ -66,62 +68,64 @@ public function testNestedBehaviors() $this->translatableListener->setTranslatableLocale('en_US'); - $vegies = $this->em->getRepository(self::CATEGORY) + $vegies = $this->em->getRepository(BehavioralCategory::class) ->find($vegies->getId()); - $translations = $this->em->getRepository(self::TRANSLATION) + $translations = $this->em->getRepository(Translation::class) ->findTranslations($vegies); - $this->assertCount(1, $translations); - $this->assertArrayHasKey('de_DE', $translations); + static::assertCount(1, $translations); + static::assertArrayHasKey('de_DE', $translations); - $this->assertArrayHasKey('title', $translations['de_DE']); - $this->assertEquals('Deutschebles', $translations['de_DE']['title']); + static::assertArrayHasKey('title', $translations['de_DE']); + static::assertSame('Deutschebles', $translations['de_DE']['title']); - $this->assertArrayHasKey('slug', $translations['de_DE']); - $this->assertEquals('deutschebles', $translations['de_DE']['slug']); + static::assertArrayHasKey('slug', $translations['de_DE']); + static::assertSame('deutschebles', $translations['de_DE']['slug']); } - public function testTranslations() + public function testTranslations(): void { $this->populateDeTranslations(); - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(BehavioralCategory::class); $vegies = $repo->find(4); - $this->assertEquals('Vegitables', $vegies->getTitle()); + static::assertSame('Vegitables', $vegies->getTitle()); $food = $vegies->getParent(); // test if proxy triggers postLoad event - $this->assertTrue($food instanceof Proxy); - $this->assertEquals('Food', $food->getTitle()); + static::assertTrue($this->em->isUninitializedObject($food)); + static::assertInstanceOf(BehavioralCategory::class, $food); + static::assertSame('Food', $food->getTitle()); $this->em->clear(); $this->translatableListener->setTranslatableLocale('de_DE'); $vegies = $repo->find(4); - $this->assertEquals('Gemรผse', $vegies->getTitle()); + static::assertSame('Gemรผse', $vegies->getTitle()); $food = $vegies->getParent(); - $this->assertTrue($food instanceof Proxy); - $this->assertEquals('Lebensmittel', $food->getTitle()); + static::assertTrue($this->em->isUninitializedObject($food)); + static::assertInstanceOf(BehavioralCategory::class, $food); + static::assertSame('Lebensmittel', $food->getTitle()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CATEGORY, - self::ARTICLE, - self::COMMENT, - self::TRANSLATION, - ); + return [ + BehavioralCategory::class, + Article::class, + Comment::class, + Translation::class, + ]; } - private function populateDeTranslations() + private function populateDeTranslations(): void { $this->translatableListener->setTranslatableLocale('de_DE'); - $repo = $this->em->getRepository(self::CATEGORY); - $food = $repo->findOneByTitle('Food'); + $repo = $this->em->getRepository(BehavioralCategory::class); + $food = $repo->findOneBy(['title' => 'Food']); $food->setTitle('Lebensmittel'); - $vegies = $repo->findOneByTitle('Vegitables'); + $vegies = $repo->findOneBy(['title' => 'Vegitables']); $vegies->setTitle('Gemรผse'); $this->em->persist($food); @@ -131,28 +135,28 @@ private function populateDeTranslations() $this->translatableListener->setTranslatableLocale('en_US'); } - private function populate() + private function populate(): void { $root = new BehavioralCategory(); - $root->setTitle("Food"); + $root->setTitle('Food'); $root2 = new BehavioralCategory(); - $root2->setTitle("Sports"); + $root2->setTitle('Sports'); $child = new BehavioralCategory(); - $child->setTitle("Fruits"); + $child->setTitle('Fruits'); $child->setParent($root); $child2 = new BehavioralCategory(); - $child2->setTitle("Vegitables"); + $child2->setTitle('Vegitables'); $child2->setParent($root); $childsChild = new BehavioralCategory(); - $childsChild->setTitle("Carrots"); + $childsChild->setTitle('Carrots'); $childsChild->setParent($child2); $potatoes = new BehavioralCategory(); - $potatoes->setTitle("Potatoes"); + $potatoes->setTitle('Potatoes'); $potatoes->setParent($child2); $this->em->persist($root); diff --git a/tests/Gedmo/Tree/TreeObjectHydratorTest.php b/tests/Gedmo/Tree/TreeObjectHydratorTest.php new file mode 100644 index 0000000000..a4c06f1a3c --- /dev/null +++ b/tests/Gedmo/Tree/TreeObjectHydratorTest.php @@ -0,0 +1,213 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; + +use Doctrine\Common\EventManager; +use Doctrine\ORM\Query; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Category; +use Gedmo\Tests\Tree\Fixture\RootCategory; +use Gedmo\Tree\Entity\Repository\NestedTreeRepository; +use Gedmo\Tree\Hydrator\ORM\TreeObjectHydrator; +use Gedmo\Tree\TreeListener; + +/** + * Tests the tree object hydrator + * + * @author Ilija Tovilo + */ +final class TreeObjectHydratorTest extends BaseTestCaseORM +{ + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $evm->addEventSubscriber(new TreeListener()); + + $this->getDefaultMockSqliteEntityManager($evm); + + $this->em->getConfiguration()->addCustomHydrationMode('tree', TreeObjectHydrator::class); + } + + public function testFullTreeHydration(): void + { + $this->populate(); + $this->em->clear(); + + $this->queryLogger->reset(); + + $repo = $this->em->getRepository(RootCategory::class); + + $result = $repo->createQueryBuilder('node') + ->orderBy('node.lft', 'ASC') + ->getQuery() + ->setHint(Query::HINT_INCLUDE_META_COLUMNS, true) + ->getResult('tree'); + + static::assertCount(1, $result); + + $food = $result[0]; + static::assertSame('Food', $food->getTitle()); + static::assertCount(4, $food->getChildren()); + + $fruits = $food->getChildren()->get(0); + static::assertSame('Fruits', $fruits->getTitle()); + static::assertCount(2, $fruits->getChildren()); + + $vegetables = $food->getChildren()->get(1); + static::assertSame('Vegetables', $vegetables->getTitle()); + static::assertCount(0, $vegetables->getChildren()); + + $milk = $food->getChildren()->get(2); + static::assertSame('Milk', $milk->getTitle()); + static::assertCount(0, $milk->getChildren()); + + $meat = $food->getChildren()->get(3); + static::assertSame('Meat', $meat->getTitle()); + static::assertCount(0, $meat->getChildren()); + + $oranges = $fruits->getChildren()->get(0); + static::assertSame('Oranges', $oranges->getTitle()); + static::assertCount(0, $oranges->getChildren()); + + $citrons = $fruits->getChildren()->get(1); + static::assertSame('Citrons', $citrons->getTitle()); + static::assertCount(0, $citrons->getChildren()); + + // Make sure only one query was executed + static::assertCount(1, $this->queryLogger->queries); + } + + public function testPartialTreeHydration(): void + { + $this->populate(); + $this->em->clear(); + + $this->queryLogger->reset(); + + /** @var NestedTreeRepository $repo */ + $repo = $this->em->getRepository(RootCategory::class); + + $fruits = $repo->findOneBy(['title' => 'Fruits']); + + $result = $repo->getChildrenQuery($fruits, false, null, 'ASC', true) + ->setHint(Query::HINT_INCLUDE_META_COLUMNS, true) + ->getResult('tree'); + + static::assertCount(1, $result); + + $fruits = $result[0]; + static::assertSame('Fruits', $fruits->getTitle()); + static::assertCount(2, $fruits->getChildren()); + + $oranges = $fruits->getChildren()->get(0); + static::assertSame('Oranges', $oranges->getTitle()); + static::assertCount(0, $oranges->getChildren()); + + $citrons = $fruits->getChildren()->get(1); + static::assertSame('Citrons', $citrons->getTitle()); + static::assertCount(0, $citrons->getChildren()); + + static::assertCount(2, $this->queryLogger->queries); + } + + public function testMultipleRootNodesTreeHydration(): void + { + $this->populate(); + $this->em->clear(); + + $this->queryLogger->reset(); + + /** @var NestedTreeRepository $repo */ + $repo = $this->em->getRepository(RootCategory::class); + + $food = $repo->findOneBy(['title' => 'Food']); + + $result = $repo->getChildrenQuery($food) + ->setHint(Query::HINT_INCLUDE_META_COLUMNS, true) + ->getResult('tree'); + + static::assertCount(4, $result); + + $fruits = $result[0]; + static::assertSame('Fruits', $fruits->getTitle()); + static::assertCount(2, $fruits->getChildren()); + + $vegetables = $result[1]; + static::assertSame('Vegetables', $vegetables->getTitle()); + static::assertCount(0, $vegetables->getChildren()); + + $milk = $result[2]; + static::assertSame('Milk', $milk->getTitle()); + static::assertCount(0, $milk->getChildren()); + + $meat = $result[3]; + static::assertSame('Meat', $meat->getTitle()); + static::assertCount(0, $meat->getChildren()); + + $oranges = $fruits->getChildren()->get(0); + static::assertSame('Oranges', $oranges->getTitle()); + static::assertCount(0, $oranges->getChildren()); + + $citrons = $fruits->getChildren()->get(1); + static::assertSame('Citrons', $citrons->getTitle()); + static::assertCount(0, $citrons->getChildren()); + + static::assertCount(2, $this->queryLogger->queries); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Category::class, + RootCategory::class, + ]; + } + + private function populate(): void + { + $repo = $this->em->getRepository(RootCategory::class); + + $food = new RootCategory(); + $food->setTitle('Food'); + + $fruits = new RootCategory(); + $fruits->setTitle('Fruits'); + + $vegetables = new RootCategory(); + $vegetables->setTitle('Vegetables'); + + $milk = new RootCategory(); + $milk->setTitle('Milk'); + + $meat = new RootCategory(); + $meat->setTitle('Meat'); + + $oranges = new RootCategory(); + $oranges->setTitle('Oranges'); + + $citrons = new RootCategory(); + $citrons->setTitle('Citrons'); + + $repo + ->persistAsFirstChild($food) + ->persistAsLastChildOf($fruits, $food) + ->persistAsLastChildOf($vegetables, $food) + ->persistAsLastChildOf($milk, $food) + ->persistAsLastChildOf($meat, $food) + ->persistAsLastChildOf($oranges, $fruits) + ->persistAsLastChildOf($citrons, $fruits); + + $this->em->flush(); + } +} diff --git a/tests/Gedmo/Tree/TreeTest.php b/tests/Gedmo/Tree/TreeTest.php index abfc4b8b19..bde7da3ee8 100644 --- a/tests/Gedmo/Tree/TreeTest.php +++ b/tests/Gedmo/Tree/TreeTest.php @@ -1,130 +1,134 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; use Doctrine\Common\EventManager; -use Tool\BaseTestCaseORM; -use Tree\Fixture\Category; -use Tree\Fixture\CategoryUuid; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\Category; +use Gedmo\Tests\Tree\Fixture\CategoryUuid; +use Gedmo\Tree\TreeListener; /** * These are tests for Tree behavior * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class TreeTest extends BaseTestCaseORM +final class TreeTest extends BaseTestCaseORM { - const CATEGORY = "Tree\\Fixture\\Category"; - const CATEGORY_UUID = "Tree\\Fixture\\CategoryUuid"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); $evm = new EventManager(); $evm->addEventSubscriber(new TreeListener()); - $this->getMockSqliteEntityManager($evm); + $this->getDefaultMockSqliteEntityManager($evm); } - public function testTheTree() + public function testTheTree(): void { - $meta = $this->em->getClassMetadata(self::CATEGORY); + $meta = $this->em->getClassMetadata(Category::class); $root = new Category(); - $root->setTitle("Root"); - $this->assertTrue($root instanceof Node); + $root->setTitle('Root'); $this->em->persist($root); $this->em->flush(); $this->em->clear(); - $root = $this->em->getRepository(self::CATEGORY)->find(1); + $root = $this->em->getRepository(Category::class)->find(1); $left = $meta->getReflectionProperty('lft')->getValue($root); $right = $meta->getReflectionProperty('rgt')->getValue($root); - $this->assertEquals(1, $left); - $this->assertEquals(2, $right); + static::assertSame(1, $left); + static::assertSame(2, $right); $child = new Category(); - $child->setTitle("child"); + $child->setTitle('child'); $child->setParent($root); $this->em->persist($child); $this->em->flush(); $this->em->clear(); - $root = $this->em->getRepository(self::CATEGORY)->find(1); + $root = $this->em->getRepository(Category::class)->find(1); $left = $meta->getReflectionProperty('lft')->getValue($root); $right = $meta->getReflectionProperty('rgt')->getValue($root); $level = $meta->getReflectionProperty('level')->getValue($root); - $this->assertEquals(1, $left); - $this->assertEquals(4, $right); - $this->assertEquals(0, $level); + static::assertSame(1, $left); + static::assertSame(4, $right); + static::assertSame(0, $level); - $child = $this->em->getRepository(self::CATEGORY)->find(2); + $child = $this->em->getRepository(Category::class)->find(2); $left = $meta->getReflectionProperty('lft')->getValue($child); $right = $meta->getReflectionProperty('rgt')->getValue($child); $level = $meta->getReflectionProperty('level')->getValue($child); - $this->assertEquals(2, $left); - $this->assertEquals(3, $right); - $this->assertEquals(1, $level); + static::assertSame(2, $left); + static::assertSame(3, $right); + static::assertSame(1, $level); $child2 = new Category(); - $child2->setTitle("child2"); + $child2->setTitle('child2'); $child2->setParent($root); $this->em->persist($child2); $this->em->flush(); $this->em->clear(); - $root = $this->em->getRepository(self::CATEGORY)->find(1); + $root = $this->em->getRepository(Category::class)->find(1); $left = $meta->getReflectionProperty('lft')->getValue($root); $right = $meta->getReflectionProperty('rgt')->getValue($root); $level = $meta->getReflectionProperty('level')->getValue($root); - $this->assertEquals(1, $left); - $this->assertEquals(6, $right); - $this->assertEquals(0, $level); + static::assertSame(1, $left); + static::assertSame(6, $right); + static::assertSame(0, $level); - $child2 = $this->em->getRepository(self::CATEGORY)->find(3); + $child2 = $this->em->getRepository(Category::class)->find(3); $left = $meta->getReflectionProperty('lft')->getValue($child2); $right = $meta->getReflectionProperty('rgt')->getValue($child2); $level = $meta->getReflectionProperty('level')->getValue($child2); - $this->assertEquals(4, $left); - $this->assertEquals(5, $right); - $this->assertEquals(1, $level); + static::assertSame(4, $left); + static::assertSame(5, $right); + static::assertSame(1, $level); $childsChild = new Category(); - $childsChild->setTitle("childs2_child"); + $childsChild->setTitle('childs2_child'); $childsChild->setParent($child2); $this->em->persist($childsChild); $this->em->flush(); $this->em->clear(); - $child2 = $this->em->getRepository(self::CATEGORY)->find(3); + $child2 = $this->em->getRepository(Category::class)->find(3); $left = $meta->getReflectionProperty('lft')->getValue($child2); $right = $meta->getReflectionProperty('rgt')->getValue($child2); $level = $meta->getReflectionProperty('level')->getValue($child2); - $this->assertEquals(4, $left); - $this->assertEquals(7, $right); - $this->assertEquals(1, $level); + static::assertSame(4, $left); + static::assertSame(7, $right); + static::assertSame(1, $level); $level = $meta->getReflectionProperty('level')->getValue($childsChild); - $this->assertEquals(2, $level); + static::assertSame(2, $level); // test updates to nodes, parent changes - $childsChild = $this->em->getRepository(self::CATEGORY)->find(4); - $child = $this->em->getRepository(self::CATEGORY)->find(2); + $childsChild = $this->em->getRepository(Category::class)->find(4); + $child = $this->em->getRepository(Category::class)->find(2); $childsChild->setTitle('childs_child'); $childsChild->setParent($child); @@ -132,14 +136,14 @@ public function testTheTree() $this->em->flush(); $this->em->clear(); - $child = $this->em->getRepository(self::CATEGORY)->find(2); + $child = $this->em->getRepository(Category::class)->find(2); $left = $meta->getReflectionProperty('lft')->getValue($child); $right = $meta->getReflectionProperty('rgt')->getValue($child); $level = $meta->getReflectionProperty('level')->getValue($child); - $this->assertEquals(2, $left); - $this->assertEquals(5, $right); - $this->assertEquals(1, $level); + static::assertSame(2, $left); + static::assertSame(5, $right); + static::assertSame(1, $level); // test deletion @@ -147,19 +151,19 @@ public function testTheTree() $this->em->flush(); $this->em->clear(); - $root = $this->em->getRepository(self::CATEGORY)->find(1); + $root = $this->em->getRepository(Category::class)->find(1); $left = $meta->getReflectionProperty('lft')->getValue($root); $right = $meta->getReflectionProperty('rgt')->getValue($root); - $this->assertEquals(1, $left); - $this->assertEquals(4, $right); + static::assertSame(1, $left); + static::assertSame(4, $right); // test persisting in any time $yetAnotherChild = new Category(); $this->em->persist($yetAnotherChild); - $yetAnotherChild->setTitle("yetanotherchild"); + $yetAnotherChild->setTitle('yetanotherchild'); $yetAnotherChild->setParent($root); - //$this->em->persist($yetAnotherChild); + // $this->em->persist($yetAnotherChild); $this->em->flush(); $this->em->clear(); @@ -167,14 +171,14 @@ public function testTheTree() $right = $meta->getReflectionProperty('rgt')->getValue($yetAnotherChild); $level = $meta->getReflectionProperty('level')->getValue($yetAnotherChild); - $this->assertEquals(4, $left); - $this->assertEquals(5, $right); - $this->assertEquals(1, $level); + static::assertSame(4, $left); + static::assertSame(5, $right); + static::assertSame(1, $level); } - public function testIssue33() + public function testIssue33(): void { - $repo = $this->em->getRepository(self::CATEGORY); + $repo = $this->em->getRepository(Category::class); $root = new Category(); $root->setTitle('root'); @@ -198,50 +202,49 @@ public function testIssue33() $this->em->flush(); $this->em->clear(); - $subNode = $repo->findOneByTitle('sub-node'); - $node1 = $repo->findOneByTitle('node1'); + $subNode = $repo->findOneBy(['title' => 'sub-node']); + $node1 = $repo->findOneBy(['title' => 'node1']); $subNode->setParent($node1); $this->em->persist($subNode); $this->em->flush(); $this->em->clear(); - $meta = $this->em->getClassMetadata(self::CATEGORY); - $subNode = $repo->findOneByTitle('sub-node'); + $meta = $this->em->getClassMetadata(Category::class); + $subNode = $repo->findOneBy(['title' => 'sub-node']); $left = $meta->getReflectionProperty('lft')->getValue($subNode); $right = $meta->getReflectionProperty('rgt')->getValue($subNode); - $this->assertEquals(3, $left); - $this->assertEquals(4, $right); + static::assertSame(3, $left); + static::assertSame(4, $right); - $node1 = $repo->findOneByTitle('node1'); + $node1 = $repo->findOneBy(['title' => 'node1']); $left = $meta->getReflectionProperty('lft')->getValue($node1); $right = $meta->getReflectionProperty('rgt')->getValue($node1); - $this->assertEquals(2, $left); - $this->assertEquals(5, $right); + static::assertSame(2, $left); + static::assertSame(5, $right); } - public function testIssue273() + public function testIssue273(): void { - $meta = $this->em->getClassMetadata(self::CATEGORY_UUID); + $meta = $this->em->getClassMetadata(CategoryUuid::class); $root = new CategoryUuid(); - $root->setTitle("Root"); - $this->assertTrue($root instanceof Node); + $root->setTitle('Root'); $this->em->persist($root); $rootId = $root->getId(); $this->em->flush(); $this->em->clear(); - $root = $this->em->getRepository(self::CATEGORY_UUID)->find($rootId); + $root = $this->em->getRepository(CategoryUuid::class)->find($rootId); $left = $meta->getReflectionProperty('lft')->getValue($root); $right = $meta->getReflectionProperty('rgt')->getValue($root); - $this->assertEquals(1, $left); - $this->assertEquals(2, $right); + static::assertSame(1, $left); + static::assertSame(2, $right); $child = new CategoryUuid(); - $child->setTitle("child"); + $child->setTitle('child'); $child->setParent($root); $this->em->persist($child); @@ -249,26 +252,26 @@ public function testIssue273() $this->em->flush(); $this->em->clear(); - $root = $this->em->getRepository(self::CATEGORY_UUID)->find($rootId); + $root = $this->em->getRepository(CategoryUuid::class)->find($rootId); $left = $meta->getReflectionProperty('lft')->getValue($root); $right = $meta->getReflectionProperty('rgt')->getValue($root); $level = $meta->getReflectionProperty('level')->getValue($root); - $this->assertEquals(1, $left); - $this->assertEquals(4, $right); - $this->assertEquals(0, $level); + static::assertSame(1, $left); + static::assertSame(4, $right); + static::assertSame(0, $level); - $child = $this->em->getRepository(self::CATEGORY_UUID)->find($childId); + $child = $this->em->getRepository(CategoryUuid::class)->find($childId); $left = $meta->getReflectionProperty('lft')->getValue($child); $right = $meta->getReflectionProperty('rgt')->getValue($child); $level = $meta->getReflectionProperty('level')->getValue($child); - $this->assertEquals(2, $left); - $this->assertEquals(3, $right); - $this->assertEquals(1, $level); + static::assertSame(2, $left); + static::assertSame(3, $right); + static::assertSame(1, $level); $child2 = new CategoryUuid(); - $child2->setTitle("child2"); + $child2->setTitle('child2'); $child2->setParent($root); $this->em->persist($child2); @@ -276,26 +279,26 @@ public function testIssue273() $this->em->flush(); $this->em->clear(); - $root = $this->em->getRepository(self::CATEGORY_UUID)->find($rootId); + $root = $this->em->getRepository(CategoryUuid::class)->find($rootId); $left = $meta->getReflectionProperty('lft')->getValue($root); $right = $meta->getReflectionProperty('rgt')->getValue($root); $level = $meta->getReflectionProperty('level')->getValue($root); - $this->assertEquals(1, $left); - $this->assertEquals(6, $right); - $this->assertEquals(0, $level); + static::assertSame(1, $left); + static::assertSame(6, $right); + static::assertSame(0, $level); - $child2 = $this->em->getRepository(self::CATEGORY_UUID)->find($child2Id); + $child2 = $this->em->getRepository(CategoryUuid::class)->find($child2Id); $left = $meta->getReflectionProperty('lft')->getValue($child2); $right = $meta->getReflectionProperty('rgt')->getValue($child2); $level = $meta->getReflectionProperty('level')->getValue($child2); - $this->assertEquals(4, $left); - $this->assertEquals(5, $right); - $this->assertEquals(1, $level); + static::assertSame(4, $left); + static::assertSame(5, $right); + static::assertSame(1, $level); $childsChild = new CategoryUuid(); - $childsChild->setTitle("childs2_child"); + $childsChild->setTitle('childs2_child'); $childsChild->setParent($child2); $this->em->persist($childsChild); @@ -303,23 +306,23 @@ public function testIssue273() $this->em->flush(); $this->em->clear(); - $child2 = $this->em->getRepository(self::CATEGORY_UUID)->find($child2Id); + $child2 = $this->em->getRepository(CategoryUuid::class)->find($child2Id); $left = $meta->getReflectionProperty('lft')->getValue($child2); $right = $meta->getReflectionProperty('rgt')->getValue($child2); $level = $meta->getReflectionProperty('level')->getValue($child2); - $this->assertEquals(4, $left); - $this->assertEquals(7, $right); - $this->assertEquals(1, $level); + static::assertSame(4, $left); + static::assertSame(7, $right); + static::assertSame(1, $level); $level = $meta->getReflectionProperty('level')->getValue($childsChild); - $this->assertEquals(2, $level); + static::assertSame(2, $level); // test updates to nodes, parent changes - $childsChild = $this->em->getRepository(self::CATEGORY_UUID)->find($childsChildId); - $child = $this->em->getRepository(self::CATEGORY_UUID)->find($childId); + $childsChild = $this->em->getRepository(CategoryUuid::class)->find($childsChildId); + $child = $this->em->getRepository(CategoryUuid::class)->find($childId); $childsChild->setTitle('childs_child'); $childsChild->setParent($child); @@ -327,14 +330,14 @@ public function testIssue273() $this->em->flush(); $this->em->clear(); - $child = $this->em->getRepository(self::CATEGORY_UUID)->find($childId); + $child = $this->em->getRepository(CategoryUuid::class)->find($childId); $left = $meta->getReflectionProperty('lft')->getValue($child); $right = $meta->getReflectionProperty('rgt')->getValue($child); $level = $meta->getReflectionProperty('level')->getValue($child); - $this->assertEquals(2, $left); - $this->assertEquals(5, $right); - $this->assertEquals(1, $level); + static::assertSame(2, $left); + static::assertSame(5, $right); + static::assertSame(1, $level); // test deletion @@ -342,19 +345,19 @@ public function testIssue273() $this->em->flush(); $this->em->clear(); - $root = $this->em->getRepository(self::CATEGORY_UUID)->find($rootId); + $root = $this->em->getRepository(CategoryUuid::class)->find($rootId); $left = $meta->getReflectionProperty('lft')->getValue($root); $right = $meta->getReflectionProperty('rgt')->getValue($root); - $this->assertEquals(1, $left); - $this->assertEquals(4, $right); + static::assertSame(1, $left); + static::assertSame(4, $right); // test persisting in any time $yetAnotherChild = new CategoryUuid(); $this->em->persist($yetAnotherChild); - $yetAnotherChild->setTitle("yetanotherchild"); + $yetAnotherChild->setTitle('yetanotherchild'); $yetAnotherChild->setParent($root); - //$this->em->persist($yetAnotherChild); + // $this->em->persist($yetAnotherChild); $this->em->flush(); $this->em->clear(); @@ -362,16 +365,16 @@ public function testIssue273() $right = $meta->getReflectionProperty('rgt')->getValue($yetAnotherChild); $level = $meta->getReflectionProperty('level')->getValue($yetAnotherChild); - $this->assertEquals(4, $left); - $this->assertEquals(5, $right); - $this->assertEquals(1, $level); + static::assertSame(4, $left); + static::assertSame(5, $right); + static::assertSame(1, $level); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::CATEGORY, - self::CATEGORY_UUID, - ); + return [ + Category::class, + CategoryUuid::class, + ]; } } diff --git a/tests/Gedmo/Uploadable/FileInfo/FileInfoArrayTest.php b/tests/Gedmo/Uploadable/FileInfo/FileInfoArrayTest.php index 8ce64e3748..726e1ac59f 100644 --- a/tests/Gedmo/Uploadable/FileInfo/FileInfoArrayTest.php +++ b/tests/Gedmo/Uploadable/FileInfo/FileInfoArrayTest.php @@ -1,23 +1,31 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\FileInfo; + +use Gedmo\Uploadable\FileInfo\FileInfoArray; +use PHPUnit\Framework\TestCase; /** * These are tests for the FileInfoArray class of the Uploadable behavior * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ - -class FileInfoArrayTest extends \PHPUnit_Framework_TestCase +final class FileInfoArrayTest extends TestCase { - /** - * @expectedException RuntimeException - */ - public function test_constructor_ifKeysAreNotValidOrSomeAreMissingThrowException() + public function testConstructorIfKeysAreNotValidOrSomeAreMissingThrowException(): void { - $fileInfo = new FileInfoArray(array()); + $this->expectException('RuntimeException'); + + new FileInfoArray([]); } } diff --git a/tests/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumericTest.php b/tests/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumericTest.php index 2f117b6c21..195e0e1be8 100644 --- a/tests/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumericTest.php +++ b/tests/Gedmo/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumericTest.php @@ -1,26 +1,32 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\FilenameGenerator; use Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorAlphanumeric; +use PHPUnit\Framework\TestCase; /** * These are tests for FilenameGeneratorAlphanumeric class * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class FilenameGeneratorAlphanumericTest extends \PHPUnit_Framework_TestCase +final class FilenameGeneratorAlphanumericTest extends TestCase { - public function testGenerator() + public function testGenerator(): void { - $generator = new FilenameGeneratorAlphanumeric(); - $filename = 'MegaName_For_A_###$$$File$$$###'; $extension = '.exe'; - $this->assertEquals('meganame-for-a-file-.exe', $generator->generate($filename, $extension)); + static::assertSame('meganame-for-a-file-.exe', FilenameGeneratorAlphanumeric::generate($filename, $extension)); } } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/Article.php b/tests/Gedmo/Uploadable/Fixture/Entity/Article.php index affa37e081..6eb19de9fa 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/Article.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/Article.php @@ -1,68 +1,94 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; -use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string") */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING)] + private ?string $title = null; /** + * @var Collection + * * @ORM\OneToMany(targetEntity="File", mappedBy="article", cascade={"persist", "remove"}) */ + #[ORM\OneToMany(targetEntity: File::class, mappedBy: 'article', cascade: ['persist', 'remove'])] private $files; + private ?string $filePath = null; + public function __construct() { $this->files = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function getFiles() + /** + * @return Collection + */ + public function getFiles(): Collection { return $this->files; } - public function addFile(File $file) + public function addFile(File $file): void { $this->files[] = $file; } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/File.php b/tests/Gedmo/Uploadable/Fixture/Entity/File.php index 862eb89d67..708cddb82a 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/File.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/File.php @@ -1,84 +1,111 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\Uploadable(allowOverwrite=true, pathMethod="getPath", callback="callbackMethod") */ +#[ORM\Entity] +#[Gedmo\Uploadable(allowOverwrite: true, pathMethod: 'getPath', callback: 'callbackMethod')] class File { /** - * @ORM\Column(name="id", type="integer") + * @var bool + */ + public $callbackWasCalled = false; + + /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", nullable=true) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, nullable: true)] + private ?string $title = null; /** * @ORM\Column(name="path", type="string") + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; /** * @ORM\ManyToOne(targetEntity="Article", inversedBy="files") * @ORM\JoinColumn(name="article_id", referencedColumnName="id") */ - private $article; - - public $callbackWasCalled = false; + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'files')] + #[ORM\JoinColumn(name: 'article_id', referencedColumnName: 'id')] + private ?Article $article = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function setArticle(Article $article) + public function setArticle(Article $article): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?Article { return $this->article; } - public function callbackMethod() + public function callbackMethod(): void { $this->callbackWasCalled = true; } - public function getPath() + public function getPath(): string { - return __DIR__.'/../../../../temp/uploadable'; + return TESTS_TEMP_DIR.'/uploadable'; } } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/FileAppendNumber.php b/tests/Gedmo/Uploadable/Fixture/Entity/FileAppendNumber.php index 003ec50d26..a9ab73d6b4 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/FileAppendNumber.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/FileAppendNumber.php @@ -1,77 +1,101 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\Uploadable(appendNumber=true, pathMethod="getPath") */ +#[ORM\Entity] +#[Gedmo\Uploadable(appendNumber: true, pathMethod: 'getPath')] class FileAppendNumber { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", nullable=true) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, nullable: true)] + private ?string $title = null; /** * @ORM\Column(name="path", type="string") + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; /** * @ORM\ManyToOne(targetEntity="Article", inversedBy="files") * @ORM\JoinColumn(name="article_id", referencedColumnName="id") */ - private $article; + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'files')] + #[ORM\JoinColumn(name: 'article_id', referencedColumnName: 'id')] + private ?Article $article = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function setArticle(Article $article) + public function setArticle(Article $article): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?Article { return $this->article; } - public function getPath() + public function getPath(): string { - return __DIR__.'/../../../../temp/uploadable'; + return TESTS_TEMP_DIR.'/uploadable'; } } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/FileAppendNumberRelative.php b/tests/Gedmo/Uploadable/Fixture/Entity/FileAppendNumberRelative.php index 42a9c60677..721869362c 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/FileAppendNumberRelative.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/FileAppendNumberRelative.php @@ -1,71 +1,96 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Uploadable\Mapping\Validator; /** * @ORM\Entity + * * @Gedmo\Uploadable(appendNumber=true, path="./", filenameGenerator="ALPHANUMERIC") */ +#[ORM\Entity] +#[Gedmo\Uploadable(appendNumber: true, path: './', filenameGenerator: Validator::FILENAME_GENERATOR_ALPHANUMERIC)] class FileAppendNumberRelative { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", nullable=true) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, nullable: true)] + private ?string $title = null; /** * @ORM\Column(name="path", type="string") + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; /** * @ORM\ManyToOne(targetEntity="Article", inversedBy="files") * @ORM\JoinColumn(name="article_id", referencedColumnName="id") */ - private $article; + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'files')] + #[ORM\JoinColumn(name: 'article_id', referencedColumnName: 'id')] + private ?Article $article = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function setArticle(Article $article) + public function setArticle(Article $article): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?Article { return $this->article; } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithAllowedTypes.php b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithAllowedTypes.php index 9794065025..d670b6886c 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithAllowedTypes.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithAllowedTypes.php @@ -1,87 +1,114 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\Uploadable(allowedTypes="text/plain,text/html") */ +#[ORM\Entity] +#[Gedmo\Uploadable(allowedTypes: 'text/plain,text/html')] class FileWithAllowedTypes { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", nullable=true) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, nullable: true)] + private ?string $title = null; /** * @ORM\Column(name="path", type="string", nullable=true) + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING, nullable: true)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; /** - * @ORM\Column(name="size", type="decimal", nullable=true) + * @ORM\Column(name="size", type="decimal", precision=10, scale=2, nullable=true) + * * @Gedmo\UploadableFileSize */ - private $fileSize; + #[ORM\Column(name: 'size', type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)] + #[Gedmo\UploadableFileSize] + private ?string $fileSize = null; /** * @ORM\ManyToOne(targetEntity="Article", inversedBy="files") * @ORM\JoinColumn(name="article_id", referencedColumnName="id") */ - private $article; + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'files')] + #[ORM\JoinColumn(name: 'article_id', referencedColumnName: 'id')] + private ?Article $article = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function setArticle(Article $article) + public function setArticle(Article $article): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?Article { return $this->article; } - public function setFileSize($size) + public function setFileSize(?string $size): void { $this->fileSize = $size; } - public function getFileSize() + public function getFileSize(): ?string { return $this->fileSize; } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithAlphanumericName.php b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithAlphanumericName.php index 3d00314d08..40ffede720 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithAlphanumericName.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithAlphanumericName.php @@ -1,46 +1,68 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Uploadable\Mapping\Validator; /** * @ORM\Entity + * * @Gedmo\Uploadable(pathMethod="getPath", filenameGenerator="ALPHANUMERIC", appendNumber=true) */ +#[ORM\Entity] +#[Gedmo\Uploadable(pathMethod: 'getPath', filenameGenerator: Validator::FILENAME_GENERATOR_ALPHANUMERIC, appendNumber: true)] class FileWithAlphanumericName { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="path", type="string", nullable=true) + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING, nullable: true)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function getPath() + public function getPath(): string { - return __DIR__.'/../../../../temp/uploadable'; + return TESTS_TEMP_DIR.'/uploadable'; } } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithCustomFilenameGenerator.php b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithCustomFilenameGenerator.php index 8f6231650c..043b3b1e9d 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithCustomFilenameGenerator.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithCustomFilenameGenerator.php @@ -1,46 +1,68 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tests\Uploadable\FakeFilenameGenerator; /** * @ORM\Entity - * @Gedmo\Uploadable(pathMethod="getPath", filenameGenerator="Gedmo\Uploadable\FakeFilenameGenerator") + * + * @Gedmo\Uploadable(pathMethod="getPath", filenameGenerator="Gedmo\Tests\Uploadable\FakeFilenameGenerator") */ +#[ORM\Entity] +#[Gedmo\Uploadable(pathMethod: 'getPath', filenameGenerator: FakeFilenameGenerator::class)] class FileWithCustomFilenameGenerator { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="path", type="string", nullable=true) + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING, nullable: true)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function getPath() + public function getPath(): string { - return __DIR__.'/../../../../temp/uploadable'; + return TESTS_TEMP_DIR.'/uploadable'; } } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithDisallowedTypes.php b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithDisallowedTypes.php index 33c2bf79f1..d88a9cb6e4 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithDisallowedTypes.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithDisallowedTypes.php @@ -1,87 +1,114 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\Uploadable(disallowedTypes="text/css, text/html") */ +#[ORM\Entity] +#[Gedmo\Uploadable(disallowedTypes: 'text/css, text/html')] class FileWithDisallowedTypes { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", nullable=true) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, nullable: true)] + private ?string $title = null; /** * @ORM\Column(name="path", type="string", nullable=true) + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING, nullable: true)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; /** - * @ORM\Column(name="size", type="decimal", nullable=true) + * @ORM\Column(name="size", type="decimal", precision=10, scale=2, nullable=true) + * * @Gedmo\UploadableFileSize */ - private $fileSize; + #[ORM\Column(name: 'size', type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)] + #[Gedmo\UploadableFileSize] + private ?string $fileSize = null; /** * @ORM\ManyToOne(targetEntity="Article", inversedBy="files") * @ORM\JoinColumn(name="article_id", referencedColumnName="id") */ - private $article; + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'files')] + #[ORM\JoinColumn(name: 'article_id', referencedColumnName: 'id')] + private ?Article $article = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function setArticle(Article $article) + public function setArticle(Article $article): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?Article { return $this->article; } - public function setFileSize($size) + public function setFileSize(?string $size): void { $this->fileSize = $size; } - public function getFileSize() + public function getFileSize(): ?string { return $this->fileSize; } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithMaxSize.php b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithMaxSize.php index 7931039a2f..0c71229b6f 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithMaxSize.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithMaxSize.php @@ -1,100 +1,130 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity - * @Gedmo\Uploadable(allowOverwrite=true, pathMethod="getPath", callback="callbackMethod", maxSize="1") + * + * @Gedmo\Uploadable(allowOverwrite=true, pathMethod="getPath", callback="callbackMethod", maxSize="2") */ +#[ORM\Entity] +#[Gedmo\Uploadable(allowOverwrite: true, pathMethod: 'getPath', callback: 'callbackMethod', maxSize: '2')] class FileWithMaxSize { /** - * @ORM\Column(name="id", type="integer") + * @var bool + */ + public $callbackWasCalled = false; + + /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string", nullable=true) */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING, nullable: true)] + private ?string $title = null; /** * @ORM\Column(name="path", type="string") + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; /** - * @ORM\Column(name="size", type="decimal") + * @ORM\Column(name="size", type="decimal", precision=10, scale=2) + * * @Gedmo\UploadableFileSize */ - private $fileSize; + #[ORM\Column(name: 'size', type: Types::DECIMAL, precision: 10, scale: 2)] + #[Gedmo\UploadableFileSize] + private ?string $fileSize = null; /** * @ORM\ManyToOne(targetEntity="Article", inversedBy="files") * @ORM\JoinColumn(name="article_id", referencedColumnName="id") */ - private $article; - - public $callbackWasCalled = false; + #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'files')] + #[ORM\JoinColumn(name: 'article_id', referencedColumnName: 'id')] + private ?Article $article = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function setArticle(Article $article) + public function setArticle(Article $article): void { $this->article = $article; } - public function getArticle() + public function getArticle(): ?Article { return $this->article; } - public function setFileSize($size) + public function setFileSize(?string $size): void { $this->fileSize = $size; } - public function getFileSize() + public function getFileSize(): ?string { return $this->fileSize; } - public function callbackMethod() + public function callbackMethod(): void { $this->callbackWasCalled = true; } - public function getPath() + public function getPath(): string { - return __DIR__.'/../../../../temp/uploadable'; + return TESTS_TEMP_DIR.'/uploadable'; } } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithSha1Name.php b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithSha1Name.php index 6133844d08..95b03ee790 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithSha1Name.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithSha1Name.php @@ -1,46 +1,68 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Uploadable\Mapping\Validator; /** * @ORM\Entity + * * @Gedmo\Uploadable(pathMethod="getPath", filenameGenerator="SHA1") */ +#[ORM\Entity] +#[Gedmo\Uploadable(pathMethod: 'getPath', filenameGenerator: Validator::FILENAME_GENERATOR_SHA1)] class FileWithSha1Name { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="path", type="string", nullable=true) + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING, nullable: true)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function getPath() + public function getPath(): string { - return __DIR__.'/../../../../temp/uploadable'; + return TESTS_TEMP_DIR.'/uploadable'; } } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithoutPath.php b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithoutPath.php index fd1ac2a3a4..125ea9da36 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/FileWithoutPath.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/FileWithoutPath.php @@ -1,40 +1,61 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\Uploadable */ +#[ORM\Entity] +#[Gedmo\Uploadable] class FileWithoutPath { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="path", type="string", nullable=true) + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING, nullable: true)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/Image.php b/tests/Gedmo/Uploadable/Fixture/Entity/Image.php index e4e504abe8..1d524b98dc 100644 --- a/tests/Gedmo/Uploadable/Fixture/Entity/Image.php +++ b/tests/Gedmo/Uploadable/Fixture/Entity/Image.php @@ -1,103 +1,131 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; /** * @ORM\Entity + * * @Gedmo\Uploadable(pathMethod="getPath") */ +#[ORM\Entity] +#[Gedmo\Uploadable(pathMethod: 'getPath')] class Image { /** - * @ORM\Column(name="id", type="integer") + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(name="title", type="string") */ - private $title; + #[ORM\Column(name: 'title', type: Types::STRING)] + private ?string $title = null; /** * @ORM\Column(name="path", type="string", nullable=true) + * * @Gedmo\UploadableFilePath */ - private $filePath; + #[ORM\Column(name: 'path', type: Types::STRING, nullable: true)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; /** - * @ORM\Column(name="size", type="decimal", nullable=true) + * @ORM\Column(name="size", type="decimal", precision=10, scale=2, nullable=true) + * * @Gedmo\UploadableFileSize */ - private $size; + #[ORM\Column(name: 'size', type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)] + #[Gedmo\UploadableFileSize] + private ?string $size = null; /** * @ORM\Column(name="mime_type", type="string", nullable=true) + * * @Gedmo\UploadableFileMimeType */ - private $mime; + #[ORM\Column(name: 'mime_type', type: Types::STRING, nullable: true)] + #[Gedmo\UploadableFileMimeType] + private ?string $mime = null; - private $useBasePath = false; + private bool $useBasePath = false; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } - public function setFilePath($filePath) + public function setFilePath(?string $filePath): void { $this->filePath = $filePath; } - public function getFilePath() + public function getFilePath(): ?string { return $this->filePath; } - public function getPath($basePath = null) + public function getPath(?string $basePath = null): string { if ($this->useBasePath) { return $basePath.'/abc/def'; } - return __DIR__.'/../../../../temp/uploadable'; + return TESTS_TEMP_DIR.'/uploadable'; } - public function setMime($mime) + public function setMime(?string $mime): void { $this->mime = $mime; } - public function getMime() + public function getMime(): ?string { return $this->mime; } - public function setSize($size) + public function setSize(?string $size): void { $this->size = $size; } - public function getSize() + public function getSize(): ?string { return $this->size; } - public function setUseBasePath($useBasePath) + public function setUseBasePath(bool $useBasePath): void { $this->useBasePath = $useBasePath; } diff --git a/tests/Gedmo/Uploadable/Fixture/Entity/ImageWithTypedProperties.php b/tests/Gedmo/Uploadable/Fixture/Entity/ImageWithTypedProperties.php new file mode 100644 index 0000000000..395c552884 --- /dev/null +++ b/tests/Gedmo/Uploadable/Fixture/Entity/ImageWithTypedProperties.php @@ -0,0 +1,130 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + * + * @Gedmo\Uploadable(pathMethod="getPath") + */ +#[ORM\Entity] +#[Gedmo\Uploadable(pathMethod: 'getPath')] +class ImageWithTypedProperties +{ + /** + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] + private ?int $id = null; + + /** + * @ORM\Column(name="title", type="string") + */ + #[ORM\Column(name: 'title', type: Types::STRING)] + private ?string $title = null; + + /** + * @ORM\Column(name="path", type="string", nullable=true) + * + * @Gedmo\UploadableFilePath + */ + #[ORM\Column(name: 'path', type: Types::STRING, nullable: true)] + #[Gedmo\UploadableFilePath] + private ?string $filePath = null; + + /** + * @ORM\Column(name="size", type="decimal", precision=10, scale=2, nullable=true) + * + * @Gedmo\UploadableFileSize + */ + #[ORM\Column(name: 'size', type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)] + #[Gedmo\UploadableFileSize] + private ?string $size = null; + + /** + * @ORM\Column(name="mime_type", type="string", nullable=true) + * + * @Gedmo\UploadableFileMimeType + */ + #[ORM\Column(name: 'mime_type', type: Types::STRING, nullable: true)] + #[Gedmo\UploadableFileMimeType] + private ?string $mime = null; + + private bool $useBasePath = false; + + public function getId(): ?int + { + return $this->id; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setFilePath(?string $filePath): void + { + $this->filePath = $filePath; + } + + public function getFilePath(): ?string + { + return $this->filePath; + } + + public function getPath(?string $basePath = null): string + { + if ($this->useBasePath) { + return $basePath.'/abc/def'; + } + + return TESTS_TEMP_DIR.'/uploadable'; + } + + public function setMime(?string $mime): void + { + $this->mime = $mime; + } + + public function getMime(): ?string + { + return $this->mime; + } + + public function setSize(?string $size): void + { + $this->size = $size; + } + + public function getSize(): ?string + { + return $this->size; + } + + public function setUseBasePath(bool $useBasePath): void + { + $this->useBasePath = $useBasePath; + } +} diff --git a/tests/Gedmo/Uploadable/Mapping/ValidatorTest.php b/tests/Gedmo/Uploadable/Mapping/ValidatorTest.php index 0013aef888..b6de1b07dd 100644 --- a/tests/Gedmo/Uploadable/Mapping/ValidatorTest.php +++ b/tests/Gedmo/Uploadable/Mapping/ValidatorTest.php @@ -1,40 +1,64 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Mapping; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\FieldMapping; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Exception\UploadableInvalidPathException; +use Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorSha1; +use Gedmo\Uploadable\Mapping\Validator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * These are tests for the Mapping Validator of the Uploadable behavior * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ - -class ValidatorTest extends \PHPUnit_Framework_TestCase +final class ValidatorTest extends TestCase { + /** + * @var ClassMetadata&MockObject + */ protected $meta; - public function setUp() + protected function setUp(): void { - $this->meta = $this->getMock('Doctrine\ORM\Mapping\ClassMetadata', array(), array(), '', false); + $this->meta = $this->getMockBuilder(ClassMetadata::class) + ->setConstructorArgs(['', null]) + ->getMock(); Validator::$enableMimeTypesConfigException = false; } - public function tearDown() + protected function tearDown(): void { Validator::$enableMimeTypesConfigException = true; } - /** - * @expectedException \Gedmo\Exception\InvalidMappingException - */ - public function test_validateField_ifFieldIsNotOfAValidTypeThrowException() + public function testValidateFieldIfFieldIsNotOfAValidTypeThrowException(): void { - $this->meta->expects($this->once()) + $this->expectException(InvalidMappingException::class); + $this->meta->expects(static::once()) ->method('getFieldMapping') - ->will($this->returnValue(array('type' => 'someType'))); + ->willReturnCallback(static function (string $fieldName) { + if (class_exists(FieldMapping::class)) { + return FieldMapping::fromMappingArray(['type' => 'someType', 'fieldName' => $fieldName, 'columnName' => $fieldName]); + } + + return ['type' => 'someType']; + }); Validator::validateField( $this->meta, @@ -44,72 +68,37 @@ public function test_validateField_ifFieldIsNotOfAValidTypeThrowException() ); } - /** - * @expectedException \Gedmo\Exception\UploadableInvalidPathException - */ - public function test_validatePath_ifPathIsNotAStringOrIsAnEmptyStringThrowException() + public function testValidatePathIfPathIsNotAStringOrIsAnEmptyStringThrowException(): void { + $this->expectException(UploadableInvalidPathException::class); Validator::validatePath(''); } - public function test_validatePath_ifPassedDirIsNotAValidDirectoryOrIsNotWriteableThrowException() + public function testValidatePathCreatesNewDirectoryWhenItNotExists(): void { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - $this->markTestSkipped('Not possible to test on Windows'); - } - - $dir = sys_get_temp_dir().'/readonly-directory-12312432423'; - mkdir($dir, 0000, true); - try { - Validator::validatePath('/'); - } catch (\Gedmo\Exception\UploadableCantWriteException $e) { - rmdir($dir); - - return; - } - - rmdir($dir); - $this->fail( - sprintf('An expected exception "%s" has not been raised.', 'Gedmo\Exception\UploadableCantWriteException') - ); - } - - public function test_validatePathCreatesNewDirectoryWhenItNotExists() - { - $dir = sys_get_temp_dir().'/new/directory-12312432423'; + $dir = TESTS_TEMP_DIR.'/new/directory-12312432423'; Validator::validatePath($dir); - $this->assertTrue(is_dir($dir)); + static::assertDirectoryExists($dir); rmdir($dir); rmdir(dirname($dir)); } - public function test_validatePath_ifPassedDirIsNotAValidDirectoryOrIsNotWriteableDoesNotThrowExceptionIfDisabled() + public function testValidateConfigurationIfNeitherFilePathFieldNorFileNameFieldIsNotDefinedThrowException(): void { - Validator::$validateWritableDirectory = false; - Validator::validatePath('/invalid/directory/12312432423'); - Validator::$validateWritableDirectory = true; - } - - /** - * @expectedException \Gedmo\Exception\InvalidMappingException - */ - public function test_validateConfiguration_ifNeitherFilePathFieldNorFileNameFieldIsNotDefinedThrowException() - { - $config = array('filePathField' => false, 'fileNameField' => false); + $this->expectException(InvalidMappingException::class); + $config = ['filePathField' => false, 'fileNameField' => false]; Validator::validateConfiguration($this->meta, $config); } - /** - * @expectedException \Gedmo\Exception\InvalidMappingException - */ - public function test_validateConfiguration_ifPathMethodIsNotAValidMethodThrowException() + public function testValidateConfigurationIfPathMethodIsNotAValidMethodThrowException(): void { - $this->meta->expects($this->once()) + $this->expectException(InvalidMappingException::class); + $this->meta->expects(static::once()) ->method('getReflectionClass') - ->will($this->returnValue(new \ReflectionClass(new FakeEntity()))); + ->willReturn(new \ReflectionClass(new FakeEntity())); - $config = array('filePathField' => 'someField', 'pathMethod' => 'invalidMethod'); + $config = ['filePathField' => 'someField', 'pathMethod' => 'invalidMethod']; Validator::validateConfiguration( $this->meta, @@ -117,16 +106,14 @@ public function test_validateConfiguration_ifPathMethodIsNotAValidMethodThrowExc ); } - /** - * @expectedException \Gedmo\Exception\InvalidMappingException - */ - public function test_validateConfiguration_ifCallbackMethodIsNotAValidMethodThrowException() + public function testValidateConfigurationIfCallbackMethodIsNotAValidMethodThrowException(): void { - $this->meta->expects($this->once()) + $this->expectException(InvalidMappingException::class); + $this->meta->expects(static::once()) ->method('getReflectionClass') - ->will($this->returnValue(new \ReflectionClass(new FakeEntity()))); + ->willReturn(new \ReflectionClass(new FakeEntity())); - $config = array('filePathField' => 'someField', 'pathMethod' => '', 'callback' => 'invalidMethod'); + $config = ['filePathField' => 'someField', 'pathMethod' => '', 'callback' => 'invalidMethod']; Validator::validateConfiguration( $this->meta, @@ -134,30 +121,34 @@ public function test_validateConfiguration_ifCallbackMethodIsNotAValidMethodThro ); } - /** - * @expectedException \Gedmo\Exception\InvalidMappingException - */ - public function test_validateConfiguration_ifFilenameGeneratorValueIsNotValidThrowException() + public function testValidateConfigurationIfFilenameGeneratorValueIsNotValidThrowException(): void { - $this->meta->expects($this->once()) + $this->expectException(InvalidMappingException::class); + $this->meta->expects(static::once()) ->method('getReflectionClass') - ->will($this->returnValue(new \ReflectionClass(new FakeEntity()))); - $this->meta->expects($this->any()) + ->willReturn(new \ReflectionClass(new FakeEntity())); + $this->meta ->method('getFieldMapping') - ->will($this->returnValue(array('type' => 'someType'))); + ->willReturnCallback(static function (string $fieldName) { + if (class_exists(FieldMapping::class)) { + return FieldMapping::fromMappingArray(['type' => 'someType', 'fieldName' => $fieldName, 'columnName' => $fieldName]); + } + + return ['type' => 'someType']; + }); - $config = array( + $config = [ 'fileMimeTypeField' => '', - 'fileSizeField' => '', - 'fileNameField' => '', - 'filePathField' => 'someField', - 'pathMethod' => '', - 'callback' => '', + 'fileSizeField' => '', + 'fileNameField' => '', + 'filePathField' => 'someField', + 'pathMethod' => '', + 'callback' => '', 'filenameGenerator' => 'invalidClass', - 'maxSize' => 0, - 'allowedTypes' => '', - 'disallowedTypes' => '', - ); + 'maxSize' => 0, + 'allowedTypes' => '', + 'disallowedTypes' => '', + ]; Validator::validateConfiguration( $this->meta, @@ -165,30 +156,34 @@ public function test_validateConfiguration_ifFilenameGeneratorValueIsNotValidThr ); } - /** - * @expectedException \Gedmo\Exception\InvalidMappingException - */ - public function test_validateConfiguration_ifFilenameGeneratorValueIsValidButDoesntImplementNeededInterfaceThrowException() + public function testValidateConfigurationIfFilenameGeneratorValueIsValidButDoesntImplementNeededInterfaceThrowException(): void { - $this->meta->expects($this->once()) + $this->expectException(InvalidMappingException::class); + $this->meta->expects(static::once()) ->method('getReflectionClass') - ->will($this->returnValue(new \ReflectionClass(new FakeEntity()))); - $this->meta->expects($this->any()) + ->willReturn(new \ReflectionClass(new FakeEntity())); + $this->meta ->method('getFieldMapping') - ->will($this->returnValue(array('type' => 'someType'))); + ->willReturnCallback(static function (string $fieldName) { + if (class_exists(FieldMapping::class)) { + return FieldMapping::fromMappingArray(['type' => 'someType', 'fieldName' => $fieldName, 'columnName' => $fieldName]); + } + + return ['type' => 'someType']; + }); - $config = array( + $config = [ 'fileMimeTypeField' => '', - 'fileSizeField' => '', - 'fileNameField' => '', - 'filePathField' => 'someField', - 'pathMethod' => '', - 'callback' => '', + 'fileSizeField' => '', + 'fileNameField' => '', + 'filePathField' => 'someField', + 'pathMethod' => '', + 'callback' => '', 'filenameGenerator' => 'DateTime', - 'maxSize' => 0, - 'allowedTypes' => '', - 'disallowedTypes' => '', - ); + 'maxSize' => 0, + 'allowedTypes' => '', + 'disallowedTypes' => '', + ]; Validator::validateConfiguration( $this->meta, @@ -196,27 +191,33 @@ public function test_validateConfiguration_ifFilenameGeneratorValueIsValidButDoe ); } - public function test_validateConfiguration_ifFilenameGeneratorValueIsValidThenDontThrowException() + public function testValidateConfigurationIfFilenameGeneratorValueIsValidThenDontThrowException(): void { - $this->meta->expects($this->once()) + $this->meta->expects(static::once()) ->method('getReflectionClass') - ->will($this->returnValue(new \ReflectionClass(new FakeEntity()))); - $this->meta->expects($this->any()) + ->willReturn(new \ReflectionClass(new FakeEntity())); + $this->meta ->method('getFieldMapping') - ->will($this->returnValue(array('type' => 'string'))); + ->willReturnCallback(static function (string $fieldName) { + if (class_exists(FieldMapping::class)) { + return FieldMapping::fromMappingArray(['type' => 'string', 'fieldName' => $fieldName, 'columnName' => $fieldName]); + } + + return ['type' => 'string']; + }); - $config = array( + $config = [ 'fileMimeTypeField' => '', - 'fileSizeField' => '', - 'fileNameField' => '', - 'filePathField' => 'someField', - 'pathMethod' => '', - 'callback' => '', + 'fileSizeField' => '', + 'fileNameField' => '', + 'filePathField' => 'someField', + 'pathMethod' => '', + 'callback' => '', 'filenameGenerator' => 'SHA1', - 'maxSize' => 0, - 'allowedTypes' => '', - 'disallowedTypes' => '', - ); + 'maxSize' => 0, + 'allowedTypes' => '', + 'disallowedTypes' => '', + ]; Validator::validateConfiguration( $this->meta, @@ -224,27 +225,33 @@ public function test_validateConfiguration_ifFilenameGeneratorValueIsValidThenDo ); } - public function test_validateConfiguration_ifFilenameGeneratorValueIsAValidClassThenDontThrowException() + public function testValidateConfigurationIfFilenameGeneratorValueIsAValidClassThenDontThrowException(): void { - $this->meta->expects($this->once()) + $this->meta->expects(static::once()) ->method('getReflectionClass') - ->will($this->returnValue(new \ReflectionClass(new FakeEntity()))); - $this->meta->expects($this->any()) + ->willReturn(new \ReflectionClass(new FakeEntity())); + $this->meta ->method('getFieldMapping') - ->will($this->returnValue(array('type' => 'string'))); + ->willReturnCallback(static function (string $fieldName) { + if (class_exists(FieldMapping::class)) { + return FieldMapping::fromMappingArray(['type' => 'string', 'fieldName' => $fieldName, 'columnName' => $fieldName]); + } - $config = array( + return ['type' => 'string']; + }); + + $config = [ 'fileMimeTypeField' => '', - 'fileSizeField' => '', - 'fileNameField' => '', - 'filePathField' => 'someField', - 'pathMethod' => '', - 'callback' => '', - 'filenameGenerator' => 'Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorSha1', - 'maxSize' => 0, - 'allowedTypes' => '', - 'disallowedTypes' => '', - ); + 'fileSizeField' => '', + 'fileNameField' => '', + 'filePathField' => 'someField', + 'pathMethod' => '', + 'callback' => '', + 'filenameGenerator' => FilenameGeneratorSha1::class, + 'maxSize' => 0, + 'allowedTypes' => '', + 'disallowedTypes' => '', + ]; Validator::validateConfiguration( $this->meta, @@ -252,25 +259,23 @@ public function test_validateConfiguration_ifFilenameGeneratorValueIsAValidClass ); } - /** - * @expectedException \Gedmo\Exception\InvalidMappingException - */ - public function test_validateConfiguration_ifMaxSizeIsLessThanZeroThenThrowException() + public function testValidateConfigurationIfMaxSizeIsLessThanZeroThenThrowException(): void { - $this->meta->expects($this->once()) + $this->expectException(InvalidMappingException::class); + $this->meta->expects(static::once()) ->method('getReflectionClass') - ->will($this->returnValue(new \ReflectionClass(new FakeEntity()))); + ->willReturn(new \ReflectionClass(new FakeEntity())); - $config = array( + $config = [ 'fileMimeTypeField' => 'someField', - 'filePathField' => 'someField', - 'fileSizeField' => '', - 'pathMethod' => '', - 'callback' => '', - 'maxSize' => -123, - 'allowedTypes' => '', - 'disallowedTypes' => '', - ); + 'filePathField' => 'someField', + 'fileSizeField' => '', + 'pathMethod' => '', + 'callback' => '', + 'maxSize' => -123, + 'allowedTypes' => '', + 'disallowedTypes' => '', + ]; Validator::validateConfiguration( $this->meta, @@ -278,27 +283,25 @@ public function test_validateConfiguration_ifMaxSizeIsLessThanZeroThenThrowExcep ); } - /** - * @expectedException \Gedmo\Exception\InvalidMappingException - */ - public function test_validateConfiguration_ifAllowedTypesAndDisallowedTypesAreSetThenThrowException() + public function testValidateConfigurationIfAllowedTypesAndDisallowedTypesAreSetThenThrowException(): void { - $this->meta->expects($this->once()) + $this->expectException(InvalidMappingException::class); + $this->meta->expects(static::once()) ->method('getReflectionClass') - ->will($this->returnValue(new \ReflectionClass(new FakeEntity()))); + ->willReturn(new \ReflectionClass(new FakeEntity())); Validator::$enableMimeTypesConfigException = true; - $config = array( + $config = [ 'fileMimeTypeField' => 'someField', - 'filePathField' => 'someField', - 'fileSizeField' => '', - 'pathMethod' => '', - 'callback' => '', - 'maxSize' => 0, - 'allowedTypes' => 'text/plain', - 'disallowedTypes' => 'text/css', - ); + 'filePathField' => 'someField', + 'fileSizeField' => '', + 'pathMethod' => '', + 'callback' => '', + 'maxSize' => 0, + 'allowedTypes' => 'text/plain', + 'disallowedTypes' => 'text/css', + ]; Validator::validateConfiguration( $this->meta, diff --git a/tests/Gedmo/Uploadable/Stub/FileInfoStub.php b/tests/Gedmo/Uploadable/Stub/FileInfoStub.php index 0a6794b206..d8a797e9e4 100644 --- a/tests/Gedmo/Uploadable/Stub/FileInfoStub.php +++ b/tests/Gedmo/Uploadable/Stub/FileInfoStub.php @@ -1,9 +1,47 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ -class FileInfoStub extends FileInfoArray +namespace Gedmo\Tests\Uploadable\Stub; + +use Gedmo\Uploadable\FileInfo\FileInfoInterface; + +final class FileInfoStub implements FileInfoInterface { + public function getTmpName() + { + throw new \BadMethodCallException('Not implemented.'); + } + + public function getName() + { + throw new \BadMethodCallException('Not implemented.'); + } + + public function getSize() + { + throw new \BadMethodCallException('Not implemented.'); + } + + public function getType() + { + throw new \BadMethodCallException('Not implemented.'); + } + + public function getError() + { + throw new \BadMethodCallException('Not implemented.'); + } + + public function isUploadedFile() + { + throw new \BadMethodCallException('Not implemented.'); + } } diff --git a/tests/Gedmo/Uploadable/Stub/MimeTypeGuesserStub.php b/tests/Gedmo/Uploadable/Stub/MimeTypeGuesserStub.php index 51218cdf26..0a53a0e99e 100644 --- a/tests/Gedmo/Uploadable/Stub/MimeTypeGuesserStub.php +++ b/tests/Gedmo/Uploadable/Stub/MimeTypeGuesserStub.php @@ -1,19 +1,31 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Stub; use Gedmo\Uploadable\MimeType\MimeTypeGuesserInterface; class MimeTypeGuesserStub implements MimeTypeGuesserInterface { + /** + * @var string|null + */ protected $mimeType; - public function __construct($mimeType) + public function __construct(?string $mimeType) { $this->mimeType = $mimeType; } - public function guess($path) + public function guess($path): ?string { return $this->mimeType; } diff --git a/tests/Gedmo/Uploadable/Stub/UploadableListenerStub.php b/tests/Gedmo/Uploadable/Stub/UploadableListenerStub.php index 7a1517adeb..a8181be78e 100644 --- a/tests/Gedmo/Uploadable/Stub/UploadableListenerStub.php +++ b/tests/Gedmo/Uploadable/Stub/UploadableListenerStub.php @@ -1,14 +1,26 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable\Stub; use Gedmo\Uploadable\UploadableListener; -class UploadableListenerStub extends UploadableListener +final class UploadableListenerStub extends UploadableListener { + /** + * @var bool + */ public $returnFalseOnMoveUploadedFile = false; - public function doMoveFile($source, $dest, $isUploadedFile = true) + public function doMoveFile($source, $dest, $isUploadedFile = true): bool { return $this->returnFalseOnMoveUploadedFile ? false : parent::doMoveFile($source, $dest, false); } diff --git a/tests/Gedmo/Uploadable/UploadableEntitySizeTypeTest.php b/tests/Gedmo/Uploadable/UploadableEntitySizeTypeTest.php new file mode 100644 index 0000000000..f24faea3d5 --- /dev/null +++ b/tests/Gedmo/Uploadable/UploadableEntitySizeTypeTest.php @@ -0,0 +1,115 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Uploadable\Fixture\Entity\ImageWithTypedProperties; +use Gedmo\Tests\Uploadable\Stub\MimeTypeGuesserStub; +use Gedmo\Tests\Uploadable\Stub\UploadableListenerStub; +use Gedmo\Uploadable\Mapping\Validator; + +/** + * This test is for Uploadable behavior with typed properties + */ +final class UploadableEntitySizeTypeTest extends BaseTestCaseORM +{ + private UploadableListenerStub $listener; + + private string $destinationTestDir; + + protected function setUp(): void + { + parent::setUp(); + + $evm = new EventManager(); + $this->listener = new UploadableListenerStub(); + $this->listener->setMimeTypeGuesser(new MimeTypeGuesserStub('text/plain')); + + $evm->addEventSubscriber($this->listener); + $config = $this->getDefaultConfiguration(); + $this->em = $this->getDefaultMockSqliteEntityManager($evm, $config); + + $this->destinationTestDir = TESTS_TEMP_DIR.'/uploadable'; + + $this->clearFilesAndDirectories(); + + Validator::validatePath($this->destinationTestDir); + } + + protected function tearDown(): void + { + $this->clearFilesAndDirectories(); + } + + public function testUploadableEntity(): void + { + $testFile = TESTS_PATH.'/data/test_for_typed_properties.txt'; + $testFilename = substr($testFile, strrpos($testFile, '/') + 1); + $testFileSize = 4; + $testFileMimeType = 'text/plain'; + + $fileInfo = [ + 'tmp_name' => $testFile, + 'name' => $testFilename, + 'size' => $testFileSize, + 'type' => $testFileMimeType, + 'error' => 0, + ]; + + $image = new ImageWithTypedProperties(); + $image->setTitle('456'); + $this->listener->addEntityFileInfo($image, $fileInfo); + + $this->em->persist($image); + $this->em->flush(); + + $this->em->refresh($image); + + $file = $image->getFilePath(); + + $this->assertPathEquals($image->getPath().'/'.$testFilename, $image->getFilePath()); + static::assertTrue(is_file($file)); + static::assertSame((string) $testFileSize, $image->getSize()); + static::assertSame($testFileMimeType, $image->getMime()); + + $this->em->remove($image); + $this->em->flush(); + + static::assertFalse(is_file($file)); + } + + protected function getUsedEntityFixtures(): array + { + return [ + ImageWithTypedProperties::class, + ]; + } + + protected function assertPathEquals(string $expected, string $path, string $message = ''): void + { + static::assertSame($expected, $path, $message); + } + + private function clearFilesAndDirectories(): void + { + if (is_dir($this->destinationTestDir)) { + $iter = new \DirectoryIterator($this->destinationTestDir); + + foreach ($iter as $fileInfo) { + if (!$fileInfo->isDot()) { + @unlink($fileInfo->getPathname()); + } + } + } + } +} diff --git a/tests/Gedmo/Uploadable/UploadableEntityTest.php b/tests/Gedmo/Uploadable/UploadableEntityTest.php index 00e0df4cb8..0b496238b6 100644 --- a/tests/Gedmo/Uploadable/UploadableEntityTest.php +++ b/tests/Gedmo/Uploadable/UploadableEntityTest.php @@ -1,72 +1,93 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Uploadable; -use Tool\BaseTestCaseORM; use Doctrine\Common\EventManager; -use Uploadable\Fixture\Entity\Image; -use Uploadable\Fixture\Entity\Article; -use Uploadable\Fixture\Entity\File; -use Uploadable\Fixture\Entity\FileWithoutPath; -use Uploadable\Fixture\Entity\FileWithSha1Name; -use Uploadable\Fixture\Entity\FileWithAlphanumericName; -use Uploadable\Fixture\Entity\FileWithCustomFilenameGenerator; -use Uploadable\Fixture\Entity\FileAppendNumber; -use Uploadable\Fixture\Entity\FileAppendNumberRelative; -use Uploadable\Fixture\Entity\FileWithMaxSize; -use Uploadable\Fixture\Entity\FileWithAllowedTypes; -use Uploadable\Fixture\Entity\FileWithDisallowedTypes; -use Gedmo\Uploadable\Stub\UploadableListenerStub; -use Gedmo\Uploadable\Stub\MimeTypeGuesserStub; +use Gedmo\Exception\InvalidArgumentException; +use Gedmo\Exception\UploadableCantWriteException; +use Gedmo\Exception\UploadableCouldntGuessMimeTypeException; +use Gedmo\Exception\UploadableException; +use Gedmo\Exception\UploadableExtensionException; +use Gedmo\Exception\UploadableFileAlreadyExistsException; +use Gedmo\Exception\UploadableFormSizeException; +use Gedmo\Exception\UploadableIniSizeException; +use Gedmo\Exception\UploadableInvalidMimeTypeException; +use Gedmo\Exception\UploadableMaxSizeException; +use Gedmo\Exception\UploadableNoFileException; +use Gedmo\Exception\UploadableNoPathDefinedException; +use Gedmo\Exception\UploadableNoTmpDirException; +use Gedmo\Exception\UploadablePartialException; +use Gedmo\Exception\UploadableUploadException; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Uploadable\Fixture\Entity\Article; +use Gedmo\Tests\Uploadable\Fixture\Entity\File; +use Gedmo\Tests\Uploadable\Fixture\Entity\FileAppendNumber; +use Gedmo\Tests\Uploadable\Fixture\Entity\FileAppendNumberRelative; +use Gedmo\Tests\Uploadable\Fixture\Entity\FileWithAllowedTypes; +use Gedmo\Tests\Uploadable\Fixture\Entity\FileWithAlphanumericName; +use Gedmo\Tests\Uploadable\Fixture\Entity\FileWithCustomFilenameGenerator; +use Gedmo\Tests\Uploadable\Fixture\Entity\FileWithDisallowedTypes; +use Gedmo\Tests\Uploadable\Fixture\Entity\FileWithMaxSize; +use Gedmo\Tests\Uploadable\Fixture\Entity\FileWithoutPath; +use Gedmo\Tests\Uploadable\Fixture\Entity\FileWithSha1Name; +use Gedmo\Tests\Uploadable\Fixture\Entity\Image; +use Gedmo\Tests\Uploadable\Stub\FileInfoStub; +use Gedmo\Tests\Uploadable\Stub\MimeTypeGuesserStub; +use Gedmo\Tests\Uploadable\Stub\UploadableListenerStub; use Gedmo\Uploadable\FileInfo\FileInfoArray; +use Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorInterface; +use Gedmo\Uploadable\Mapping\Validator; /** * These are tests for Uploadable behavior * * @author Gustavo Falco * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class UploadableEntityTest extends BaseTestCaseORM +final class UploadableEntityTest extends BaseTestCaseORM { - const IMAGE_CLASS = 'Uploadable\Fixture\Entity\Image'; - const ARTICLE_CLASS = 'Uploadable\Fixture\Entity\Article'; - const FILE_CLASS = 'Uploadable\Fixture\Entity\File'; - const FILE_APPEND_NUMBER_CLASS = 'Uploadable\Fixture\Entity\FileAppendNumber'; - const FILE_APPEND_NUMBER__RELATIVE_PATH_CLASS = 'Uploadable\Fixture\Entity\FileAppendNumberRelative'; - const FILE_WITHOUT_PATH_CLASS = 'Uploadable\Fixture\Entity\FileWithoutPath'; - const FILE_WITH_SHA1_NAME_CLASS = 'Uploadable\Fixture\Entity\FileWithSha1Name'; - const FILE_WITH_ALPHANUMERIC_NAME_CLASS = 'Uploadable\Fixture\Entity\FileWithAlphanumericName'; - const FILE_WITH_CUSTOM_FILENAME_GENERATOR_CLASS = 'Uploadable\Fixture\Entity\FileWithCustomFilenameGenerator'; - const FILE_WITH_MAX_SIZE_CLASS = 'Uploadable\Fixture\Entity\FileWithMaxSize'; - const FILE_WITH_ALLOWED_TYPES_CLASS = 'Uploadable\Fixture\Entity\FileWithAllowedTypes'; - const FILE_WITH_DISALLOWED_TYPES_CLASS = 'Uploadable\Fixture\Entity\FileWithDisallowedTypes'; + private const FILE_WITH_SHA1_NAME_CLASS = FileWithSha1Name::class; - /** - * @var UploadableListener - */ - private $listener; - private $testFile; - private $testFile2; - private $testFile3; - private $testFileWithoutExt; - private $testFileWithSpaces; - private $destinationTestDir; - private $destinationTestFile; - private $destinationTestFile2; - private $destinationTestFile3; - private $destinationTestFileWithoutExt; - private $destinationTestFileWithSpaces; - private $testFilename; - private $testFilename2; - private $testFilename3; - private $testFilenameWithoutExt; - private $testFilenameWithSpaces; - private $testFileSize; - private $testFileMimeType; - - protected function setUp() + private UploadableListenerStub $listener; + + private string $testFile; + + private string $testFile2; + + private string $testFile3; + + private string $testFileWithoutExt; + + private string $testFileWithSpaces; + + private string $destinationTestDir; + + private string $destinationTestFile; + + private string $testFilename; + + private string $testFilename2; + + private string $testFilename3; + + private string $testFilenameWithoutExt; + + private string $testFilenameWithSpaces; + + private int $testFileSize; + + private string $testFileMimeType; + + protected function setUp(): void { parent::setUp(); @@ -75,19 +96,15 @@ protected function setUp() $this->listener->setMimeTypeGuesser(new MimeTypeGuesserStub('text/plain')); $evm->addEventSubscriber($this->listener); - $config = $this->getMockAnnotatedConfig(); - $this->em = $this->getMockSqliteEntityManager($evm, $config); - $this->testFile = __DIR__.'/../../data/test.txt'; - $this->testFile2 = __DIR__.'/../../data/test2.txt'; - $this->testFile3 = __DIR__.'/../../data/test_3.txt'; - $this->testFileWithoutExt = __DIR__.'/../../data/test4'; - $this->testFileWithSpaces = __DIR__.'/../../data/test with spaces.txt'; - $this->destinationTestDir = __DIR__.'/../../temp/uploadable'; + $config = $this->getDefaultConfiguration(); + $this->em = $this->getDefaultMockSqliteEntityManager($evm, $config); + $this->testFile = TESTS_PATH.'/data/test.txt'; + $this->testFile2 = TESTS_PATH.'/data/test2.txt'; + $this->testFile3 = TESTS_PATH.'/data/test_3.txt'; + $this->testFileWithoutExt = TESTS_PATH.'/data/test4'; + $this->testFileWithSpaces = TESTS_PATH.'/data/test with spaces.txt'; + $this->destinationTestDir = TESTS_TEMP_DIR.'/uploadable'; $this->destinationTestFile = $this->destinationTestDir.'/test.txt'; - $this->destinationTestFile2 = $this->destinationTestDir.'/test2.txt'; - $this->destinationTestFile3 = $this->destinationTestDir.'/test_3.txt'; - $this->destinationTestFileWithoutExt = $this->destinationTestDir.'/test4'; - $this->destinationTestFileWithSpaces = $this->destinationTestDir.'/test with spaces'; $this->testFilename = substr($this->testFile, strrpos($this->testFile, '/') + 1); $this->testFilename2 = substr($this->testFile2, strrpos($this->testFile2, '/') + 1); $this->testFilename3 = substr($this->testFile3, strrpos($this->testFile3, '/') + 1); @@ -98,17 +115,15 @@ protected function setUp() $this->clearFilesAndDirectories(); - if (!is_dir($this->destinationTestDir)) { - mkdir($this->destinationTestDir); - }; + Validator::validatePath($this->destinationTestDir); } - public function tearDown() + protected function tearDown(): void { $this->clearFilesAndDirectories(); } - public function testUploadableEntity() + public function testUploadableEntity(): void { // INSERTION of an Uploadable Entity @@ -119,7 +134,7 @@ public function testUploadableEntity() $this->em->persist($image); $this->em->flush(); - $this->assertNull($image->getFilePath()); + static::assertNull($image->getFilePath()); // If there is an uploaded file, we process it $fileInfo = $this->generateUploadedFile(); @@ -137,9 +152,9 @@ public function testUploadableEntity() $firstFile = $image2->getFilePath(); $this->assertPathEquals($image2->getPath().'/'.$fileInfo['name'], $image2->getFilePath()); - $this->assertTrue(is_file($firstFile)); - $this->assertEquals($fileInfo['size'], $image2->getSize()); - $this->assertEquals($fileInfo['type'], $image2->getMime()); + static::assertTrue(is_file($firstFile)); + static::assertSame((string) $fileInfo['size'], $image2->getSize()); + static::assertSame($fileInfo['type'], $image2->getMime()); // UPDATE of an Uploadable Entity @@ -157,19 +172,19 @@ public function testUploadableEntity() $lastFile = $image2->getFilePath(); $this->assertPathEquals($image2->getPath().'/'.$fileInfo['name'], $image2->getFilePath()); - $this->assertTrue(is_file($lastFile)); + static::assertTrue(is_file($lastFile)); // First file should be removed on update - $this->assertFalse(is_file($firstFile)); + static::assertFalse(is_file($firstFile)); // REMOVAL of an Uploadable Entity $this->em->remove($image2); $this->em->flush(); - $this->assertFalse(is_file($lastFile)); + static::assertFalse(is_file($lastFile)); } - public function testUploadableEntityWithCompositePath() + public function testUploadableEntityWithCompositePath(): void { // We set the default path on the listener $this->listener->setDefaultPath($this->destinationTestDir); @@ -191,9 +206,9 @@ public function testUploadableEntityWithCompositePath() $firstFile = $image2->getFilePath(); $this->assertPathEquals($image2->getPath($this->destinationTestDir).'/'.$fileInfo['name'], $image2->getFilePath()); - $this->assertTrue(is_file($firstFile)); - $this->assertEquals($fileInfo['size'], $image2->getSize()); - $this->assertEquals($fileInfo['type'], $image2->getMime()); + static::assertTrue(is_file($firstFile)); + static::assertSame((string) $fileInfo['size'], $image2->getSize()); + static::assertSame($fileInfo['type'], $image2->getMime()); // UPDATE of an Uploadable Entity @@ -211,21 +226,21 @@ public function testUploadableEntityWithCompositePath() $lastFile = $image2->getFilePath(); $this->assertPathEquals($image2->getPath($this->destinationTestDir).'/'.$fileInfo['name'], $image2->getFilePath()); - $this->assertTrue(is_file($lastFile)); + static::assertTrue(is_file($lastFile)); // First file should be removed on update - $this->assertFalse(is_file($firstFile)); + static::assertFalse(is_file($firstFile)); // REMOVAL of an Uploadable Entity $this->em->remove($image2); $this->em->flush(); - $this->assertFalse(is_file($lastFile)); + static::assertFalse(is_file($lastFile)); } - public function testEntityWithUploadableEntities() + public function testEntityWithUploadableEntities(): void { - $artRepo = $this->em->getRepository(self::ARTICLE_CLASS); + $artRepo = $this->em->getRepository(Article::class); $article = new Article(); $article->setTitle('Test'); @@ -237,11 +252,9 @@ public function testEntityWithUploadableEntities() $article->addFile($file2); $article->addFile($file3); - $filesArrayIndex = 'file'; - - $fileInfo = $this->generateUploadedFile($filesArrayIndex); - $fileInfo2 = $this->generateUploadedFile($filesArrayIndex); - $fileInfo3 = $this->generateUploadedFile($filesArrayIndex); + $fileInfo = $this->generateUploadedFile(); + $fileInfo2 = $this->generateUploadedFile(); + $fileInfo3 = $this->generateUploadedFile(); $this->listener->addEntityFileInfo($file1, $fileInfo); $this->listener->addEntityFileInfo($file2, $fileInfo2); @@ -251,7 +264,7 @@ public function testEntityWithUploadableEntities() $this->em->flush(); - $art = $artRepo->findOneByTitle('Test'); + $art = $artRepo->findOneBy(['title' => 'Test']); $files = $art->getFiles(); $file1Path = $file1->getPath().'/'.$fileInfo['name']; $file2Path = $file2->getPath().'/'.$fileInfo['name']; @@ -262,11 +275,9 @@ public function testEntityWithUploadableEntities() $this->assertPathEquals($file3Path, $files[2]->getFilePath()); } - /** - * @expectedException Gedmo\Exception\UploadableNoPathDefinedException - */ - public function testNoPathDefinedOnEntityOrListenerThrowsException() + public function testNoPathDefinedOnEntityOrListenerThrowsException(): void { + $this->expectException(UploadableNoPathDefinedException::class); $file = new FileWithoutPath(); $fileInfo = $this->generateUploadedFile(); @@ -277,7 +288,7 @@ public function testNoPathDefinedOnEntityOrListenerThrowsException() $this->em->flush(); } - public function testNoPathDefinedOnEntityButDefinedOnListenerUsesDefaultPath() + public function testNoPathDefinedOnEntityButDefinedOnListenerUsesDefaultPath(): void { // We set the default path on the listener $this->listener->setDefaultPath($this->destinationTestDir); @@ -295,7 +306,7 @@ public function testNoPathDefinedOnEntityButDefinedOnListenerUsesDefaultPath() $this->assertPathEquals($this->destinationTestFile, $file->getFilePath()); } - public function testCallbackIsCalledIfItsSetOnEntity() + public function testCallbackIsCalledIfItsSetOnEntity(): void { $file = new File(); $fileInfo = $this->generateUploadedFile(); @@ -305,15 +316,15 @@ public function testCallbackIsCalledIfItsSetOnEntity() $this->em->persist($file); $this->em->flush(); - $this->assertTrue($file->callbackWasCalled); + static::assertTrue($file->callbackWasCalled); } /** * @dataProvider uploadExceptionsProvider */ - public function testUploadExceptions($error, $exceptionClass) + public function testUploadExceptions(int $error, string $exceptionClass): void { - $this->setExpectedException($exceptionClass); + $this->expectException($exceptionClass); $file = new File(); $fileInfo = $this->generateUploadedFile(); @@ -325,9 +336,9 @@ public function testUploadExceptions($error, $exceptionClass) $this->em->flush(); } - public function testSettingAnotherDefaultFileInfoClass() + public function testSettingAnotherDefaultFileInfoClass(): void { - $fileInfoStubClass = 'Gedmo\Uploadable\Stub\FileInfoStub'; + $fileInfoStubClass = FileInfoStub::class; $this->listener->setDefaultFileInfoClass($fileInfoStubClass); @@ -337,10 +348,10 @@ public function testSettingAnotherDefaultFileInfoClass() $this->listener->addEntityFileInfo($file, $fileInfo); $fileInfo = $this->listener->getEntityFileInfo($file); - $this->assertInstanceOf($fileInfoStubClass, $fileInfo); + static::assertInstanceOf($fileInfoStubClass, $fileInfo); } - public function testFileWithFilenameSha1Generator() + public function testFileWithFilenameSha1Generator(): void { $file = new FileWithSha1Name(); $fileInfo = $this->generateUploadedFile(); @@ -355,13 +366,13 @@ public function testFileWithFilenameSha1Generator() $sha1String = substr($file->getFilePath(), strrpos($file->getFilePath(), '/') + 1); $sha1String = str_replace('.txt', '', $sha1String); - $this->assertRegExp('/[a-z0-9]{40}/', $sha1String); + static::assertMatchesRegularExpression('/[a-z0-9]{40}/', $sha1String); } - public function testFileWithFilenameAlphanumericGenerator() + public function testFileWithFilenameAlphanumericGenerator(): void { $file = new FileWithAlphanumericName(); - $fileInfo = $this->generateUploadedFile('image', $this->testFile3, $this->testFilename3); + $fileInfo = $this->generateUploadedFile($this->testFile3, $this->testFilename3); $this->listener->addEntityFileInfo($file, $fileInfo); @@ -372,10 +383,10 @@ public function testFileWithFilenameAlphanumericGenerator() $filename = substr($file->getFilePath(), strrpos($file->getFilePath(), '/') + 1); - $this->assertEquals('test-3.txt', $filename); + static::assertSame('test-3.txt', $filename); } - public function testFileWithCustomFilenameGenerator() + public function testFileWithCustomFilenameGenerator(): void { $file = new FileWithCustomFilenameGenerator(); $fileInfo = $this->generateUploadedFile(); @@ -389,13 +400,13 @@ public function testFileWithCustomFilenameGenerator() $filename = substr($file->getFilePath(), strrpos($file->getFilePath(), '/') + 1); - $this->assertEquals('123.txt', $filename); + static::assertSame('123.txt', $filename); } - public function testUploadFileWithoutExtension() + public function testUploadFileWithoutExtension(): void { $file = new File(); - $fileInfo = $this->generateUploadedFile('image', $this->testFileWithoutExt, $this->testFilenameWithoutExt); + $fileInfo = $this->generateUploadedFile($this->testFileWithoutExt, $this->testFilenameWithoutExt); $this->listener->addEntityFileInfo($file, $fileInfo); @@ -409,14 +420,48 @@ public function testUploadFileWithoutExtension() $this->assertPathEquals($filePath, $file->getFilePath()); } - /** - * @expectedException Gedmo\Exception\UploadableFileAlreadyExistsException - */ - public function testFileAlreadyExistsException() + public function testCanUploadTwoEntities(): void + { + // create two entities: File and Image + $file = new File(); + $fileInfo = $this->generateUploadedFile($this->testFile, $this->testFilename); + $this->listener->addEntityFileInfo($file, $fileInfo); + + $image = new Image(); + $image->setTitle('test image'); + $imageInfo = $this->generateUploadedFile($this->testFile2, $this->testFilename2); + $this->listener->addEntityFileInfo($image, $imageInfo); + + $this->em->persist($file); + $this->em->persist($image); + $this->em->flush(); + + // update uploaded files on both entities + $this->listener->addEntityFileInfo( + $file, + $this->generateUploadedFile($this->testFile3, $this->testFilename3) + ); + $this->listener->addEntityFileInfo( + $image, + $this->generateUploadedFile($this->testFileWithoutExt, $this->testFilenameWithoutExt) + ); + + $this->em->persist($file); + $this->em->persist($image); + $this->em->flush(); + $this->em->refresh($file); + $this->em->refresh($image); + + static::assertFileExists($file->getFilePath()); + static::assertFileExists($image->getFilePath()); + } + + public function testFileAlreadyExistsException(): void { + $this->expectException(UploadableFileAlreadyExistsException::class); $file = new Image(); $file->setTitle('test'); - $fileInfo = $this->generateUploadedFile('image', $this->testFileWithoutExt, $this->testFilenameWithoutExt); + $fileInfo = $this->generateUploadedFile($this->testFileWithoutExt, $this->testFilenameWithoutExt); $this->listener->addEntityFileInfo($file, $fileInfo); @@ -428,12 +473,32 @@ public function testFileAlreadyExistsException() $this->em->flush(); } - public function test_removeFile_ifItsNotAFileThenReturnFalse() + public function testRemoveFileIfItsNotAFileThenReturnFalse(): void + { + static::assertFalse($this->listener->removeFile('non_existent_file')); + } + + /** + * @return array> + */ + public static function dataProvider_testMoveFileUsingAppendNumberOptionAppendsNumberToFilenameIfItAlreadyExists(): array { - $this->assertFalse($this->listener->removeFile('non_existent_file')); + return [ + 'With extension' => [ + 'Filename' => 'test.txt', + 'Expected filename' => 'test-2.txt', + ], + 'Without extension' => [ + 'Filename' => 'test', + 'Expected filename' => 'test-2', + ], + ]; } - public function test_moveFile_usingAppendNumberOptionAppendsNumberToFilenameIfItAlreadyExists() + /** + * @dataProvider dataProvider_testMoveFileUsingAppendNumberOptionAppendsNumberToFilenameIfItAlreadyExists + */ + public function testMoveFileUsingAppendNumberOptionAppendsNumberToFilenameIfItAlreadyExists(string $filename, string $expectedFilename): void { $file = new FileAppendNumber(); $file2 = new FileAppendNumber(); @@ -441,7 +506,7 @@ public function test_moveFile_usingAppendNumberOptionAppendsNumberToFilenameIfIt $file->setTitle('test'); $file2->setTitle('test2'); - $fileInfo = $this->generateUploadedFile(); + $fileInfo = $this->generateUploadedFile(null, $filename); $this->listener->addEntityFileInfo($file, $fileInfo); @@ -455,22 +520,20 @@ public function test_moveFile_usingAppendNumberOptionAppendsNumberToFilenameIfIt $this->em->refresh($file2); - $filename = substr($file2->getFilePath(), strrpos($file2->getFilePath(), '/') + 1); - - $this->assertEquals('test-2.txt', $filename); + static::assertSame($expectedFilename, basename($file2->getFilePath())); } - public function test_moveFile_usingAppendNumberOptionAppendsNumberToFilenameIfItAlreadyExistsRelativePath() + public function testMoveFileUsingAppendNumberOptionAppendsNumberToFilenameIfItAlreadyExistsRelativePath(): void { $currDir = __DIR__; - chdir(realpath(__DIR__.'/../../temp/uploadable')); + chdir(realpath(TESTS_TEMP_DIR.'/uploadable')); $file = new FileAppendNumber(); $file2 = new FileAppendNumberRelative(); $file->setTitle('test'); $file2->setTitle('test2'); - $fileInfo = $this->generateUploadedFile('image', realpath(__DIR__.'/../../../tests/data/test'), 'test'); + $fileInfo = $this->generateUploadedFile(realpath(TESTS_PATH.'/data/test'), 'test'); $this->listener->addEntityFileInfo($file, $fileInfo); $this->em->persist($file); @@ -483,16 +546,14 @@ public function test_moveFile_usingAppendNumberOptionAppendsNumberToFilenameIfIt $this->em->refresh($file2); - $this->assertEquals('./test-2', $file2->getFilePath()); + static::assertSame('./test-2', $file2->getFilePath()); chdir($currDir); } - /** - * @expectedException Gedmo\Exception\UploadableUploadException - */ - public function test_moveFile_ifUploadedFileCantBeMovedThrowException() + public function testMoveFileIfUploadedFileCantBeMovedThrowException(): void { + $this->expectException(UploadableUploadException::class); $this->listener->returnFalseOnMoveUploadedFile = true; $file = new Image(); @@ -505,27 +566,23 @@ public function test_moveFile_ifUploadedFileCantBeMovedThrowException() $this->em->flush(); } - /** - * @expectedException RuntimeException - */ - public function test_addEntityFileInfo_ifFileInfoIsNotValidThrowException() + public function testAddEntityFileInfoIfFileInfoIsNotValidThrowException(): void { + $this->expectException('RuntimeException'); + + /** @phpstan-ignore-next-line argument.type */ $this->listener->addEntityFileInfo(new Image(), 'invalidFileInfo'); } - /** - * @expectedException RuntimeException - */ - public function test_getEntityFileInfo_ifTheresNoFileInfoForEntityThrowException() + public function testGetEntityFileInfoIfTheresNoFileInfoForEntityThrowException(): void { + $this->expectException('RuntimeException'); $this->listener->getEntityFileInfo(new Image()); } - /** - * @expectedException Gedmo\Exception\UploadableMaxSizeException - */ - public function test_fileExceedingMaximumAllowedSizeThrowsException() + public function testFileExceedingMaximumAllowedSizeThrowsException(): void { + $this->expectException(UploadableMaxSizeException::class); // We set the default path on the listener $this->listener->setDefaultPath($this->destinationTestDir); @@ -538,14 +595,14 @@ public function test_fileExceedingMaximumAllowedSizeThrowsException() $this->em->flush(); } - public function test_fileNotExceedingMaximumAllowedSizeDoesntThrowException() + public function testFileNotExceedingMaximumAllowedSizeDoesntThrowException(): void { // We set the default path on the listener $this->listener->setDefaultPath($this->destinationTestDir); $file = new FileWithMaxSize(); - $size = 0.0001; - $fileInfo = $this->generateUploadedFile('image', false, false, array('size' => $size)); + $size = 1; + $fileInfo = $this->generateUploadedFile(null, null, ['size' => $size]); $this->listener->addEntityFileInfo($file, $fileInfo); @@ -554,14 +611,12 @@ public function test_fileNotExceedingMaximumAllowedSizeDoesntThrowException() $this->em->refresh($file); - $this->assertEquals($size, $file->getFileSize()); + static::assertSame((string) $size, $file->getFileSize()); } - /** - * @expectedException Gedmo\Exception\UploadableCouldntGuessMimeTypeException - */ - public function test_ifMimeTypeGuesserCantResolveTypeThrowException() + public function testIfMimeTypeGuesserCantResolveTypeThrowException(): void { + $this->expectException(UploadableCouldntGuessMimeTypeException::class); // We set the default path on the listener $this->listener->setDefaultPath($this->destinationTestDir); $this->listener->setMimeTypeGuesser(new MimeTypeGuesserStub(null)); @@ -575,11 +630,9 @@ public function test_ifMimeTypeGuesserCantResolveTypeThrowException() $this->em->flush(); } - /** - * @expectedException Gedmo\Exception\UploadableInvalidMimeTypeException - */ - public function test_allowedTypesOption_ifMimeTypeIsInvalidThrowException() + public function testAllowedTypesOptionIfMimeTypeIsInvalidThrowException(): void { + $this->expectException(UploadableInvalidMimeTypeException::class); // We set the default path on the listener $this->listener->setDefaultPath($this->destinationTestDir); $this->listener->setMimeTypeGuesser(new MimeTypeGuesserStub('text/css')); @@ -593,25 +646,9 @@ public function test_allowedTypesOption_ifMimeTypeIsInvalidThrowException() $this->em->flush(); } - public function test_allowedTypesOption_ifMimeTypeIsValidThenDontThrowException() - { - // We set the default path on the listener - $this->listener->setDefaultPath($this->destinationTestDir); - - $file = new FileWithAllowedTypes(); - $fileInfo = $this->generateUploadedFile(); - - $this->listener->addEntityFileInfo($file, $fileInfo); - - $this->em->persist($file); - $this->em->flush(); - } - - /** - * @expectedException Gedmo\Exception\UploadableInvalidMimeTypeException - */ - public function test_disallowedTypesOption_ifMimeTypeIsInvalidThrowException() + public function testDisallowedTypesOptionIfMimeTypeIsInvalidThrowException(): void { + $this->expectException(UploadableInvalidMimeTypeException::class); // We set the default path on the listener $this->listener->setDefaultPath($this->destinationTestDir); $this->listener->setMimeTypeGuesser(new MimeTypeGuesserStub('text/css')); @@ -625,46 +662,33 @@ public function test_disallowedTypesOption_ifMimeTypeIsInvalidThrowException() $this->em->flush(); } - public function test_disallowedTypesOption_ifMimeTypeIsValidThenDontThrowException() - { - // We set the default path on the listener - $this->listener->setDefaultPath($this->destinationTestDir); - $this->listener->setMimeTypeGuesser(new MimeTypeGuesserStub('video/jpeg')); - - $file = new FileWithDisallowedTypes(); - $fileInfo = $this->generateUploadedFile(); - - $this->listener->addEntityFileInfo($file, $fileInfo); - - $this->em->persist($file); - $this->em->flush(); - } - /** - * @expectedException Gedmo\Exception\InvalidArgumentException + * @param mixed $class + * * @dataProvider invalidFileInfoClassesProvider */ - public function test_setDefaultFileInfoClass_throwExceptionIfInvalidClassArePassed($class) + public function testSetDefaultFileInfoClassThrowExceptionIfInvalidClassArePassed($class): void { + $this->expectException(InvalidArgumentException::class); $this->listener->setDefaultFileInfoClass($class); } - public function test_setDefaultFileInfoClass_setClassIfClassIsValid() + public function testSetDefaultFileInfoClassSetClassIfClassIsValid(): void { - $validClass = 'Gedmo\\Uploadable\\FileInfo\\FileInfoArray'; + $validClass = FileInfoArray::class; $this->listener->setDefaultFileInfoClass($validClass); - $this->assertEquals($validClass, $this->listener->getDefaultFileInfoClass()); + static::assertSame($validClass, $this->listener->getDefaultFileInfoClass()); } - public function test_useGeneratedFilenameWhenAppendingNumbers() + public function testUseGeneratedFilenameWhenAppendingNumbers(): void { // We set the default path on the listener $this->listener->setDefaultPath($this->destinationTestDir); $file = new FileWithAlphanumericName(); - $fileInfo = $this->generateUploadedFile('file', $this->testFileWithSpaces, $this->testFilenameWithSpaces); + $fileInfo = $this->generateUploadedFile($this->testFileWithSpaces, $this->testFilenameWithSpaces); $this->listener->addEntityFileInfo($file, $fileInfo); @@ -688,68 +712,85 @@ public function test_useGeneratedFilenameWhenAppendingNumbers() } // Data Providers - public function invalidFileInfoClassesProvider() - { - return array( - array(''), - array(false), - array(null), - array('FakeFileInfo'), - array(array()), - array(new \DateTime()), - ); - } - public function uploadExceptionsProvider() + /** + * @return list + */ + public static function invalidFileInfoClassesProvider(): array { - return array( - array(1, 'Gedmo\Exception\UploadableIniSizeException'), - array(2, 'Gedmo\Exception\UploadableFormSizeException'), - array(3, 'Gedmo\Exception\UploadablePartialException'), - array(4, 'Gedmo\Exception\UploadableNoFileException'), - array(6, 'Gedmo\Exception\UploadableNoTmpDirException'), - array(7, 'Gedmo\Exception\UploadableCantWriteException'), - array(8, 'Gedmo\Exception\UploadableExtensionException'), - array(999, 'Gedmo\Exception\UploadableUploadException'), - ); + return [ + [''], + [false], + [null], + ['FakeFileInfo'], + [[]], + [new \DateTime()], + ]; } - // Util + /** + * @return list + * + * @phpstan-return list}> + */ + public static function uploadExceptionsProvider(): array + { + return [ + [1, UploadableIniSizeException::class], + [2, UploadableFormSizeException::class], + [3, UploadablePartialException::class], + [4, UploadableNoFileException::class], + [6, UploadableNoTmpDirException::class], + [7, UploadableCantWriteException::class], + [8, UploadableExtensionException::class], + [999, UploadableUploadException::class], + ]; + } + + protected function getUsedEntityFixtures(): array + { + return [ + Image::class, + Article::class, + File::class, + FileWithoutPath::class, + FileAppendNumber::class, + FileAppendNumberRelative::class, + FileWithAlphanumericName::class, + self::FILE_WITH_SHA1_NAME_CLASS, + FileWithCustomFilenameGenerator::class, + FileWithMaxSize::class, + FileWithAllowedTypes::class, + FileWithDisallowedTypes::class, + ]; + } - private function generateUploadedFile($index = 'image', $filePath = false, $filename = false, array $info = array()) + protected function assertPathEquals(string $expected, string $path, string $message = ''): void { - $defaultInfo = array( - 'tmp_name' => !$filePath ? $this->testFile : $filePath, - 'name' => !$filename ? $this->testFilename : $filename, - 'size' => $this->testFileSize, - 'type' => $this->testFileMimeType, - 'error' => 0, - ); - - $info = array_merge($defaultInfo, $info); - - return $info; + static::assertSame($expected, $path, $message); } - protected function getUsedEntityFixtures() + // Util + + /** + * @param array $info + * + * @return array + */ + private function generateUploadedFile(?string $filePath = null, ?string $filename = null, array $info = []): array { - return array( - self::IMAGE_CLASS, - self::ARTICLE_CLASS, - self::FILE_CLASS, - self::FILE_WITHOUT_PATH_CLASS, - self::FILE_APPEND_NUMBER_CLASS, - self::FILE_APPEND_NUMBER__RELATIVE_PATH_CLASS, - self::FILE_WITH_ALPHANUMERIC_NAME_CLASS, - self::FILE_WITH_SHA1_NAME_CLASS, - self::FILE_WITH_CUSTOM_FILENAME_GENERATOR_CLASS, - self::FILE_WITH_MAX_SIZE_CLASS, - self::FILE_WITH_ALLOWED_TYPES_CLASS, - self::FILE_WITH_DISALLOWED_TYPES_CLASS, - ); + $defaultInfo = [ + 'tmp_name' => $filePath ?? $this->testFile, + 'name' => $filename ?? $this->testFilename, + 'size' => $this->testFileSize, + 'type' => $this->testFileMimeType, + 'error' => 0, + ]; + + return array_merge($defaultInfo, $info); } - private function clearFilesAndDirectories() + private function clearFilesAndDirectories(): void { if (is_dir($this->destinationTestDir)) { $iter = new \DirectoryIterator($this->destinationTestDir); @@ -761,20 +802,15 @@ private function clearFilesAndDirectories() } } } - - protected function assertPathEquals($expected, $path, $message = '') - { - $this->assertEquals($expected, $path, $message); - } } class FakeFileInfo { } -class FakeFilenameGenerator implements \Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorInterface +class FakeFilenameGenerator implements FilenameGeneratorInterface { - public static function generate($filename, $extension, $object = null) + public static function generate($filename, $extension, $object = null): string { return '123.txt'; } diff --git a/tests/Gedmo/Wrapper/EntityWrapperTest.php b/tests/Gedmo/Wrapper/EntityWrapperTest.php index ab58125118..15b5679e51 100644 --- a/tests/Gedmo/Wrapper/EntityWrapperTest.php +++ b/tests/Gedmo/Wrapper/EntityWrapperTest.php @@ -1,103 +1,181 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Wrapper; -use Tool\BaseTestCaseORM; use Doctrine\Common\EventManager; -use Wrapper\Fixture\Entity\Article; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Wrapper\Fixture\Entity\Article; +use Gedmo\Tests\Wrapper\Fixture\Entity\Composite; +use Gedmo\Tests\Wrapper\Fixture\Entity\CompositeRelation; use Gedmo\Tool\Wrapper\EntityWrapper; /** * Entity wrapper tests * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class EntityWrapperTest extends BaseTestCaseORM +final class EntityWrapperTest extends BaseTestCaseORM { - const ARTICLE = "Wrapper\\Fixture\\Entity\\Article"; - - protected function setUp() + protected function setUp(): void { parent::setUp(); - $this->getMockSqliteEntityManager(new EventManager()); + $this->getDefaultMockSqliteEntityManager(new EventManager()); $this->populate(); } - public function testManaged() + public function testManaged(): void { - $test = $this->em->find(self::ARTICLE, array('id' => 1)); - $this->assertInstanceOf(self::ARTICLE, $test); + $test = $this->em->find(Article::class, ['id' => 1]); + static::assertInstanceOf(Article::class, $test); $wrapped = new EntityWrapper($test, $this->em); - $this->assertEquals(1, $wrapped->getIdentifier()); - $this->assertEquals('test', $wrapped->getPropertyValue('title')); + static::assertSame(1, $wrapped->getIdentifier()); + static::assertSame('test', $wrapped->getPropertyValue('title')); $wrapped->setPropertyValue('title', 'changed'); - $this->assertEquals('changed', $wrapped->getPropertyValue('title')); + static::assertSame('changed', $wrapped->getPropertyValue('title')); - $this->assertTrue($wrapped->hasValidIdentifier()); + static::assertTrue($wrapped->hasValidIdentifier()); } - public function testProxy() + public function testProxy(): void { $this->em->clear(); - $test = $this->em->getReference(self::ARTICLE, array('id' => 1)); - $this->assertInstanceOf('Doctrine\\ORM\\Proxy\\Proxy', $test); + $test = $this->em->getReference(Article::class, ['id' => 1]); + static::assertTrue($this->em->isUninitializedObject($test)); $wrapped = new EntityWrapper($test, $this->em); $id = $wrapped->getIdentifier(false); - $this->assertTrue(is_array($id)); - $this->assertCount(1, $id); - $this->assertArrayHasKey('id', $id); - $this->assertEquals(1, $id['id']); + static::assertIsArray($id); + static::assertCount(1, $id); + static::assertArrayHasKey('id', $id); + static::assertSame(1, $id['id']); + + static::assertSame('test', $wrapped->getPropertyValue('title')); + } + + public function testComposite(): void + { + $test = $this->em->getReference(Composite::class, ['one' => 1, 'two' => 2]); + static::assertInstanceOf(Composite::class, $test); + $wrapped = new EntityWrapper($test, $this->em); - $this->assertEquals('test', $wrapped->getPropertyValue('title')); + $id = $wrapped->getIdentifier(false); + static::assertIsArray($id); + static::assertCount(2, $id); + static::assertArrayHasKey('one', $id); + static::assertArrayHasKey('two', $id); + static::assertSame(1, $id['one']); + static::assertSame(2, $id['two']); + + $id = $wrapped->getIdentifier(false, true); + static::assertIsString($id); + static::assertSame('1 2', $id); + + static::assertSame('test', $wrapped->getPropertyValue('title')); } - public function testDetachedEntity() + public function testCompositeRelation(): void { - $test = $this->em->find(self::ARTICLE, array('id' => 1)); + $art1 = $this->em->getReference(Article::class, ['id' => 1]); + $test = $this->em->getReference(CompositeRelation::class, ['article' => $art1->getId(), 'status' => 2]); + static::assertInstanceOf(CompositeRelation::class, $test); + $wrapped = new EntityWrapper($test, $this->em); + + $id = $wrapped->getIdentifier(false); + static::assertIsArray($id); + static::assertCount(2, $id); + static::assertArrayHasKey('article', $id); + static::assertArrayHasKey('status', $id); + + $id = $wrapped->getIdentifier(false, true); + static::assertIsString($id); + static::assertSame('1 2', $id); + + static::assertSame('test', $wrapped->getPropertyValue('title')); + } + + public function testDetachedEntity(): void + { + $test = $this->em->find(Article::class, ['id' => 1]); + $this->em->clear(); + $wrapped = new EntityWrapper($test, $this->em); + + static::assertSame(1, $wrapped->getIdentifier()); + static::assertSame('test', $wrapped->getPropertyValue('title')); + } + + public function testDetachedProxy(): void + { + $test = $this->em->getReference(Article::class, ['id' => 1]); $this->em->clear(); $wrapped = new EntityWrapper($test, $this->em); - $this->assertEquals(1, $wrapped->getIdentifier()); - $this->assertEquals('test', $wrapped->getPropertyValue('title')); + static::assertSame(1, $wrapped->getIdentifier()); + static::assertSame('test', $wrapped->getPropertyValue('title')); } - public function testDetachedProxy() + public function testDetachedCompositeRelation(): void { - $test = $this->em->getReference(self::ARTICLE, array('id' => 1)); + $test = $this->em->getReference(CompositeRelation::class, ['article' => 1, 'status' => 2]); $this->em->clear(); $wrapped = new EntityWrapper($test, $this->em); - $this->assertEquals(1, $wrapped->getIdentifier()); - $this->assertEquals('test', $wrapped->getPropertyValue('title')); + static::assertSame('1 2', $wrapped->getIdentifier(false, true)); + static::assertSame('test', $wrapped->getPropertyValue('title')); } - public function testSomeFunctions() + public function testCompositeRelationProxy(): void + { + $this->em->clear(); + $art1 = $this->em->getReference(Article::class, ['id' => 1]); + $test = $this->em->getReference(CompositeRelation::class, ['article' => $art1->getId(), 'status' => 2]); + static::assertTrue($this->em->isUninitializedObject($test)); + $wrapped = new EntityWrapper($test, $this->em); + + static::assertSame('1 2', $wrapped->getIdentifier(false, true)); + static::assertSame('test', $wrapped->getPropertyValue('title')); + } + + public function testSomeFunctions(): void { $test = new Article(); $wrapped = new EntityWrapper($test, $this->em); - $wrapped->populate(array('title' => 'test')); - $this->assertEquals('test', $wrapped->getPropertyValue('title')); + $test->setTitle('test'); + static::assertSame('test', $wrapped->getPropertyValue('title')); - $this->assertFalse($wrapped->hasValidIdentifier()); + static::assertFalse($wrapped->hasValidIdentifier()); } - protected function getUsedEntityFixtures() + protected function getUsedEntityFixtures(): array { - return array( - self::ARTICLE, - ); + return [ + Article::class, + Composite::class, + CompositeRelation::class, + ]; } - private function populate() + private function populate(): void { - $test = new Article(); - $test->setTitle("test"); - $this->em->persist($test); + $article = new Article(); + $article->setTitle('test'); + $this->em->persist($article); + $composite = new Composite(1, 2); + $composite->setTitle('test'); + $this->em->persist($composite); + $compositeRelation = new CompositeRelation($article, 2); + $compositeRelation->setTitle('test'); + $this->em->persist($compositeRelation); $this->em->flush(); } } diff --git a/tests/Gedmo/Wrapper/Fixture/Document/Article.php b/tests/Gedmo/Wrapper/Fixture/Document/Article.php index ac7e4aa81d..eaca534c08 100644 --- a/tests/Gedmo/Wrapper/Fixture/Document/Article.php +++ b/tests/Gedmo/Wrapper/Fixture/Document/Article.php @@ -1,33 +1,50 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Wrapper\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; +use Doctrine\ODM\MongoDB\Types\Type; /** * @MongoODM\Document(collection="articles") */ +#[MongoODM\Document(collection: 'article')] class Article { - /** @MongoODM\Id */ + /** + * @var string|null + * + * @MongoODM\Id + */ + #[MongoODM\Id] private $id; /** - * @MongoODM\String + * @MongoODM\Field(type="string") */ - private $title; + #[MongoODM\Field(type: Type::STRING)] + private ?string $title = null; - public function getId() + public function getId(): ?string { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Wrapper/Fixture/Entity/Article.php b/tests/Gedmo/Wrapper/Fixture/Entity/Article.php index 49697dcc2e..2e302dd7b3 100644 --- a/tests/Gedmo/Wrapper/Fixture/Entity/Article.php +++ b/tests/Gedmo/Wrapper/Fixture/Entity/Article.php @@ -1,37 +1,54 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Wrapper\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ +#[ORM\Entity] class Article { /** + * @var int|null + * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] private $id; /** * @ORM\Column(length=128) */ - private $title; + #[ORM\Column(length: 128)] + private ?string $title = null; - public function getId() + public function getId(): ?int { return $this->id; } - public function setTitle($title) + public function setTitle(?string $title): void { $this->title = $title; } - public function getTitle() + public function getTitle(): ?string { return $this->title; } diff --git a/tests/Gedmo/Wrapper/Fixture/Entity/Composite.php b/tests/Gedmo/Wrapper/Fixture/Entity/Composite.php new file mode 100644 index 0000000000..d750772a41 --- /dev/null +++ b/tests/Gedmo/Wrapper/Fixture/Entity/Composite.php @@ -0,0 +1,68 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Wrapper\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class Composite +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + private int $one; + + /** + * @ORM\Id + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + private int $two; + + /** + * @ORM\Column(length=128) + */ + #[ORM\Column(length: 128)] + private ?string $title = null; + + public function __construct(int $one, int $two) + { + $this->one = $one; + $this->two = $two; + } + + public function getOne(): int + { + return $this->one; + } + + public function getTwo(): int + { + return $this->two; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/tests/Gedmo/Wrapper/Fixture/Entity/CompositeRelation.php b/tests/Gedmo/Wrapper/Fixture/Entity/CompositeRelation.php new file mode 100644 index 0000000000..1158e756bc --- /dev/null +++ b/tests/Gedmo/Wrapper/Fixture/Entity/CompositeRelation.php @@ -0,0 +1,72 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Wrapper\Fixture\Entity; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + */ +#[ORM\Entity] +class CompositeRelation +{ + /** + * @var Article + * + * @todo: add type hint when https://github.com/doctrine/orm/issues/8255 is solved + * + * @ORM\Id + * @ORM\ManyToOne(targetEntity="Gedmo\Tests\Wrapper\Fixture\Entity\Article") + */ + #[ORM\Id] + #[ORM\ManyToOne(targetEntity: Article::class)] + private $article; + + /** + * @ORM\Id + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + private int $status; + + /** + * @ORM\Column(length=128) + */ + #[ORM\Column(length: 128)] + private ?string $title = null; + + public function __construct(Article $articleOne, int $status) + { + $this->article = $articleOne; + $this->status = $status; + } + + public function getArticle(): Article + { + return $this->article; + } + + public function getStatus(): int + { + return $this->status; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/tests/Gedmo/Wrapper/MongoDocumentWrapperTest.php b/tests/Gedmo/Wrapper/MongoDocumentWrapperTest.php index 77f68b2566..53916a8f45 100644 --- a/tests/Gedmo/Wrapper/MongoDocumentWrapperTest.php +++ b/tests/Gedmo/Wrapper/MongoDocumentWrapperTest.php @@ -1,93 +1,104 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Wrapper; -use Tool\BaseTestCaseMongoODM; use Doctrine\Common\EventManager; -use Wrapper\Fixture\Document\Article; +use Gedmo\Tests\Tool\BaseTestCaseMongoODM; +use Gedmo\Tests\Wrapper\Fixture\Document\Article; use Gedmo\Tool\Wrapper\MongoDocumentWrapper; /** * Mongo Document wrapper tests * * @author Gediminas Morkevicius - * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -class MongoDocumentWrapperTest extends BaseTestCaseMongoODM +final class MongoDocumentWrapperTest extends BaseTestCaseMongoODM { - const ARTICLE = "Wrapper\\Fixture\\Document\\Article"; - private $articleId; + private ?string $articleId = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->getMockDocumentManager(new EventManager()); $this->populate(); } - public function testManaged() + public function testManaged(): void { - $test = $this->dm->find(self::ARTICLE, $this->articleId); - $this->assertInstanceOf(self::ARTICLE, $test); + $test = $this->dm->find(Article::class, $this->articleId); + static::assertInstanceOf(Article::class, $test); $wrapped = new MongoDocumentWrapper($test, $this->dm); - $this->assertEquals($this->articleId, $wrapped->getIdentifier()); - $this->assertEquals('test', $wrapped->getPropertyValue('title')); + static::assertSame($this->articleId, $wrapped->getIdentifier()); + static::assertSame('test', $wrapped->getPropertyValue('title')); $wrapped->setPropertyValue('title', 'changed'); - $this->assertEquals('changed', $wrapped->getPropertyValue('title')); + static::assertSame('changed', $wrapped->getPropertyValue('title')); - $this->assertTrue($wrapped->hasValidIdentifier()); + static::assertTrue($wrapped->hasValidIdentifier()); } - public function testProxy() + public function testProxy(): void { $this->dm->clear(); - $test = $this->dm->getReference(self::ARTICLE, $this->articleId); - $this->assertInstanceOf('Doctrine\\ODM\\MongoDB\\Proxy\\Proxy', $test); + $test = $this->dm->getReference(Article::class, $this->articleId); + + if (method_exists($this->dm, 'isUninitializedObject')) { + static::assertTrue($this->dm->isUninitializedObject($test)); + } + + static::assertInstanceOf(Article::class, $test); $wrapped = new MongoDocumentWrapper($test, $this->dm); $id = $wrapped->getIdentifier(false); - $this->assertEquals($this->articleId, $id); + static::assertSame($this->articleId, $id); - $this->assertEquals('test', $wrapped->getPropertyValue('title')); + static::assertSame('test', $wrapped->getPropertyValue('title')); } - public function testDetachedEntity() + public function testDetachedEntity(): void { - $test = $this->dm->find(self::ARTICLE, $this->articleId); + $test = $this->dm->find(Article::class, $this->articleId); $this->dm->clear(); $wrapped = new MongoDocumentWrapper($test, $this->dm); - $this->assertEquals($this->articleId, $wrapped->getIdentifier()); - $this->assertEquals('test', $wrapped->getPropertyValue('title')); + static::assertSame($this->articleId, $wrapped->getIdentifier()); + static::assertSame('test', $wrapped->getPropertyValue('title')); } - public function testDetachedProxy() + public function testDetachedProxy(): void { - $test = $this->dm->getReference(self::ARTICLE, $this->articleId); + $test = $this->dm->getReference(Article::class, $this->articleId); $this->dm->clear(); $wrapped = new MongoDocumentWrapper($test, $this->dm); - $this->assertEquals($this->articleId, $wrapped->getIdentifier()); - $this->assertEquals('test', $wrapped->getPropertyValue('title')); + static::assertSame($this->articleId, $wrapped->getIdentifier()); + static::assertSame('test', $wrapped->getPropertyValue('title')); } - public function testSomeFunctions() + public function testSomeFunctions(): void { $test = new Article(); $wrapped = new MongoDocumentWrapper($test, $this->dm); - $wrapped->populate(array('title' => 'test')); - $this->assertEquals('test', $wrapped->getPropertyValue('title')); + $test->setTitle('test'); + static::assertSame('test', $wrapped->getPropertyValue('title')); - $this->assertFalse($wrapped->hasValidIdentifier()); + static::assertFalse($wrapped->hasValidIdentifier()); } - private function populate() + private function populate(): void { $test = new Article(); - $test->setTitle("test"); + $test->setTitle('test'); $this->dm->persist($test); $this->dm->flush(); $this->articleId = $test->getId(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 68baa2ceb0..db25b31b03 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,62 +1,53 @@ http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ -/** +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\PsrCachedReader; +use Doctrine\DBAL\Types\Type; +use Doctrine\Deprecations\Deprecation; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Symfony\Bridge\Doctrine\Types\UuidType; +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +/* * This is bootstrap for phpUnit unit tests, * use README.md for more details * * @author Gediminas Morkevicius * @author Christoph Krรคmer * @link http://www.gediminasm.org - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ define('TESTS_PATH', __DIR__); -define('TESTS_TEMP_DIR', __DIR__.'/temp'); -define('VENDOR_PATH', realpath(__DIR__.'/../vendor')); +define('TESTS_TEMP_DIR', sys_get_temp_dir().'/doctrine-extension-tests'); -if (!class_exists('PHPUnit_Framework_TestCase') || - version_compare(PHPUnit_Runner_Version::id(), '3.5') < 0 -) { - die('PHPUnit framework is required, at least 3.5 version'); +if (!is_dir(TESTS_TEMP_DIR)) { + mkdir(TESTS_TEMP_DIR, 0755, true); } -if (!class_exists('PHPUnit_Framework_MockObject_MockBuilder')) { - die('PHPUnit MockObject plugin is required, at least 1.0.8 version'); +require dirname(__DIR__).'/vendor/autoload.php'; + +if (class_exists(AnnotationReader::class)) { + $_ENV['annotation_reader'] = new PsrCachedReader(new AnnotationReader(), new ArrayAdapter()); + AnnotationReader::addGlobalIgnoredName('note'); + + // With ORM 3 and `doctrine/annotations` installed together, have the annotations library ignore the ORM's mapping namespace + if (!class_exists(AnnotationDriver::class)) { + AnnotationReader::addGlobalIgnoredNamespace('Doctrine\ORM\Mapping'); + } } -/** @var $loader ClassLoader */ -$loader = require __DIR__.'/../vendor/autoload.php'; - -$loader->add('Gedmo\\Mapping\\Mock', __DIR__); -$loader->add('Tool', __DIR__.'/Gedmo'); -// fixture namespaces -$loader->add('Translator\\Fixture', __DIR__.'/Gedmo'); -$loader->add('Translatable\\Fixture', __DIR__.'/Gedmo'); -$loader->add('Timestampable\\Fixture', __DIR__.'/Gedmo'); -$loader->add('Blameable\\Fixture', __DIR__.'/Gedmo'); -$loader->add('IpTraceable\\Fixture', __DIR__.'/Gedmo'); -$loader->add('Tree\\Fixture', __DIR__.'/Gedmo'); -$loader->add('Sluggable\\Fixture', __DIR__.'/Gedmo'); -$loader->add('Sortable\\Fixture', __DIR__.'/Gedmo'); -$loader->add('Mapping\\Fixture', __DIR__.'/Gedmo'); -$loader->add('Loggable\\Fixture', __DIR__.'/Gedmo'); -$loader->add('SoftDeleteable\\Fixture', __DIR__.'/Gedmo'); -$loader->add('Uploadable\\Fixture', __DIR__.'/Gedmo'); -$loader->add('Wrapper\\Fixture', __DIR__.'/Gedmo'); -$loader->add('ReferenceIntegrity\\Fixture', __DIR__.'/Gedmo'); -$loader->add('References\\Fixture', __DIR__.'/Gedmo'); -// stubs -$loader->add('Gedmo\\Uploadable\\Stub', __DIR__); - -AnnotationRegistry::registerLoader(array($loader, 'loadClass')); -Gedmo\DoctrineExtensions::registerAnnotations(); - -$reader = new AnnotationReader(); -$reader = new CachedReader($reader, new ArrayCache()); -$_ENV['annotation_reader'] = $reader; +Type::addType('uuid', UuidType::class); + +// Ignore unfixable deprecations +Deprecation::ignoreDeprecations( + 'https://github.com/doctrine-extensions/DoctrineExtensions/pull/2772', // Ignore annotations deprecations from self +); diff --git a/tests/data/test_for_typed_properties.txt b/tests/data/test_for_typed_properties.txt new file mode 100644 index 0000000000..bcfb9f8cbf --- /dev/null +++ b/tests/data/test_for_typed_properties.txt @@ -0,0 +1 @@ +test for typed properties diff --git a/tests/phpunit.xml.dist b/tests/phpunit.xml.dist deleted file mode 100644 index 7cdca832e8..0000000000 --- a/tests/phpunit.xml.dist +++ /dev/null @@ -1,69 +0,0 @@ - - - - - ./Gedmo/Translatable/ - - - ./Gedmo/Sluggable/ - - - ./Gedmo/Sortable/ - - - ./Gedmo/Tree/ - - - ./Gedmo/Timestampable/ - - - ./Gedmo/Blameable/ - - - ./Gedmo/IpTraceable/ - - - ./Gedmo/Mapping/ - - - ./Gedmo/Loggable/ - - - ./Gedmo/Sortable/ - - - ./Gedmo/Wrapper/ - - - ./Gedmo/Translator/ - - - ./Gedmo/SoftDeleteable/ - - - ./Gedmo/Uploadable/ - - - ./Gedmo/ReferenceIntegrity/ - - - ./Gedmo/References/ - - - - - - ../lib - - - diff --git a/tests/symfony-deprecations-baseline b/tests/symfony-deprecations-baseline new file mode 100644 index 0000000000..2316da77a5 --- /dev/null +++ b/tests/symfony-deprecations-baseline @@ -0,0 +1,6 @@ +# Ignore MongoDB deprecations from low dependencies +%Passing an integer mode to "MongoDB\\Driver\\ReadPreference::__construct" is deprecated% +# Ignore deprecated ORM 2.x proxies +%class implements "Doctrine\\ORM\\Proxy\\Proxy" that is deprecated% +# Ignore symfony/var-dumper lazy ghost deprecations (required for PHP 8.3 and earlier compat) +%Using ProxyHelper::generateLazyGhost() is deprecated% diff --git a/tests/temp/.empty b/tests/temp/.empty deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/travis/php.ini b/tests/travis/php.ini deleted file mode 100644 index 4edf37a79c..0000000000 --- a/tests/travis/php.ini +++ /dev/null @@ -1,2 +0,0 @@ -extension = mongo.so -apc.enable_cli=1