+
+```
+$ 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**
+[](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/continuous-integration.yml)
+[](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/qa.yml)
+[](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/coding-standards.yml)
+[](https://packagist.org/packages/gedmo/doctrine-extensions)
-[](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' => '
';
+ }
+
+ 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 ('\