diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..13a07c68c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +/tests export-ignore +/phpcs.xml.dist export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.scrutinizer.yml export-ignore +/.travis export-ignore +/.travis.yml export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 6153131fc..366a64ea9 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,5 +1,5 @@ -#github: [goetas,schmittjoh] -patreon: goetas +github: [goetas] +#patreon: goetas #open_collective: jms-serializer #ko_fi: # Replace with a single Ko-fi username #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..e237ead65 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,59 @@ +# yamllint disable rule:line-length +# yamllint disable rule:braces + +name: Continuous Integration + +on: + pull_request: + push: + branches: + - main + - master + +jobs: + tests: + runs-on: ${{ matrix.operating-system }} + + strategy: + matrix: + operating-system: [ubuntu-latest] + php-version: [7.2, 7.3, 7.4] + dependencies: [''] + include: + - { operating-system: 'ubuntu-latest', php-version: '8.0', dependencies: '--ignore-platform-req=php' } + + name: CI on ${{ matrix.operating-system }} with PHP ${{ matrix.php-version }} ${{ matrix.dependencies }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer:v2 + extensions: pdo_sqlite + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('composer.*') }}-${{ matrix.dependencies }} + restore-keys: | + composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('composer.*') }}- + composer-${{ runner.os }}-${{ matrix.php-version }}- + composer-${{ runner.os }}- + composer- + + - name: Install dependencies + run: | + composer update --no-interaction --prefer-dist --no-progress ${{ matrix.dependencies }} + + - name: Run tests + run: | + vendor/bin/phpunit diff --git a/.travis.yml b/.travis.yml index 86cc82814..4baf70d70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,14 +16,21 @@ env: matrix: include: - php: 7.2 - - php: 7.3 - - php: "7.4snapshot" + env: PHPUNIT_FLAGS='--coverage-clover clover' - php: 7.2 env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable' - php: 7.2 env: TARGET=docs - php: 7.2 env: TARGET=cs + + - php: 7.3 + - php: 7.4 + + - php: 7.4 + env: VERY_LATEST=1 + - php: 8.0snapshot + env: COMPOSER_FLAGS='--ignore-platform-req=php' fast_finish: true install: diff --git a/.travis/install_test.sh b/.travis/install_test.sh index 67a8a8d06..f30fa7279 100755 --- a/.travis/install_test.sh +++ b/.travis/install_test.sh @@ -4,8 +4,8 @@ set -ex composer self-update -if [[ $TRAVIS_PHP_VERSION = '7.2' ]]; then PHPUNIT_FLAGS="--coverage-clover clover"; else PHPUNIT_FLAGS=""; fi if [[ $TRAVIS_PHP_VERSION != '7.2' ]]; then phpenv config-rm xdebug.ini || true; fi -if [[ $TRAVIS_PHP_VERSION == '7.4'* ]]; then composer require --dev --no-update "symfony/cache:^4.4@dev"; fi + +if [[ $VERY_LATEST == '1' ]]; then composer remove --dev doctrine/phpcr-odm --no-update -n; fi composer update $COMPOSER_FLAGS -n diff --git a/CHANGELOG.md b/CHANGELOG.md index 86cd0dd2b..938077936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1152 +1,439 @@ -# Change Log +# Changelog -## [3.2.0](https://github.com/schmittjoh/serializer/tree/3.2.0) (2019-09-04) -**Fixed bugs:** - -- PHP7.4: Deprecated warning - serializationContext.php on line 152 [\#1111](https://github.com/schmittjoh/serializer/issues/1111) +## [3.10.0](https://github.com/schmittjoh/serializer/tree/3.10.0) (2020-10-29) -**Closed issues:** +**Implemented enhancements:** -- StaticPropertyMetadata first constructor argument not nullable [\#1116](https://github.com/schmittjoh/serializer/issues/1116) -- Add support for PSR-7 URIInterface objects [\#1115](https://github.com/schmittjoh/serializer/issues/1115) -- Upgraded 2.4 -\> 3.4 / Symfony 4.3.3 [\#1112](https://github.com/schmittjoh/serializer/issues/1112) -- Empty namespace [\#1087](https://github.com/schmittjoh/serializer/issues/1087) -- Format constants \(JSON, XML\) [\#1079](https://github.com/schmittjoh/serializer/issues/1079) -- @ExclusionPolicy\(policy="ALL"\) causes PHP notice message [\#1073](https://github.com/schmittjoh/serializer/issues/1073) +- Allow null to be visited if is a root object [\#1250](https://github.com/schmittjoh/serializer/pull/1250) ([goetas](https://github.com/goetas)) +- Resolve collections from DocBlock [\#1214](https://github.com/schmittjoh/serializer/pull/1214) ([dgafka](https://github.com/dgafka)) **Merged pull requests:** -- Explain once and for all the use of StaticPropertyMetadata [\#1118](https://github.com/schmittjoh/serializer/pull/1118) ([goetas](https://github.com/goetas)) -- PHP 7.4 compatibility [\#1113](https://github.com/schmittjoh/serializer/pull/1113) ([goetas](https://github.com/goetas)) -- Fix typos in UPGRADING.md [\#1107](https://github.com/schmittjoh/serializer/pull/1107) ([jdreesen](https://github.com/jdreesen)) -- Fix exclusion policy bug [\#1106](https://github.com/schmittjoh/serializer/pull/1106) ([spam312sn](https://github.com/spam312sn)) -- Add Doctrine 2 immutable datetime types to field mapping. [\#1104](https://github.com/schmittjoh/serializer/pull/1104) ([Sonny812](https://github.com/Sonny812)) - -## [3.1.1](https://github.com/schmittjoh/serializer/tree/3.1.1) (2019-06-28) -**Fixed bugs:** - -- Could not deserialize object if all properties have not type [\#1102](https://github.com/schmittjoh/serializer/issues/1102) +- Bump CS [\#1249](https://github.com/schmittjoh/serializer/pull/1249) ([simPod](https://github.com/simPod)) +- Removed redundant property initialization [\#1232](https://github.com/schmittjoh/serializer/pull/1232) ([xepozz](https://github.com/xepozz)) -**Merged pull requests:** -- Revert "Move type check when deserializing into the graph navigator" [\#1103](https://github.com/schmittjoh/serializer/pull/1103) ([goetas](https://github.com/goetas)) +## [3.9.0](https://github.com/schmittjoh/serializer/tree/3.9.0) (2020-08-26) -## [3.1.0](https://github.com/schmittjoh/serializer/tree/3.1.0) (2019-06-24) **Implemented enhancements:** -- Add support for iterable and Iterator [\#1096](https://github.com/schmittjoh/serializer/pull/1096) ([simPod](https://github.com/simPod)) -- Implement "empty" XML namespace handling [\#1095](https://github.com/schmittjoh/serializer/pull/1095) ([discordier](https://github.com/discordier)) -- Move type check when deserializing into the graph navigator [\#1080](https://github.com/schmittjoh/serializer/pull/1080) ([goetas](https://github.com/goetas)) -- Allow loading different YAML extensions [\#1078](https://github.com/schmittjoh/serializer/pull/1078) ([scaytrase](https://github.com/scaytrase)) +- Add support for skippable \(de\)serialization handlers [\#1238](https://github.com/schmittjoh/serializer/pull/1238) ([bobvandevijver](https://github.com/bobvandevijver)) +- added support for milliseconds in DateInterval deserialization [\#1234](https://github.com/schmittjoh/serializer/pull/1234) ([ivoba](https://github.com/ivoba)) **Fixed bugs:** -- Fix for failing doctrine object constructor on embeddable class [\#1031](https://github.com/schmittjoh/serializer/pull/1031) ([notrix](https://github.com/notrix)) +- Do not load entities when deserializing if their identifier is not ex… [\#1247](https://github.com/schmittjoh/serializer/pull/1247) ([goetas](https://github.com/goetas)) +- Do not use excluded fields when fetching entities [\#1246](https://github.com/schmittjoh/serializer/pull/1246) ([goetas](https://github.com/goetas)) +- Ensure accessors are cached per property when using reflection [\#1237](https://github.com/schmittjoh/serializer/pull/1237) ([goetas](https://github.com/goetas)) **Closed issues:** -- Behavior serializeNull -\> not always honored in 2.\* \(but was in 1.\*\) [\#1101](https://github.com/schmittjoh/serializer/issues/1101) -- Support for iterable [\#1094](https://github.com/schmittjoh/serializer/issues/1094) -- Prevent deserialisation with missing required field [\#1090](https://github.com/schmittjoh/serializer/issues/1090) -- Allow using @XmlValue together with @Accessor/@AccessType [\#1083](https://github.com/schmittjoh/serializer/issues/1083) -- Support \*.yaml extension [\#1077](https://github.com/schmittjoh/serializer/issues/1077) +- Annotation cache does not honor naming strategy [\#1244](https://github.com/schmittjoh/serializer/issues/1244) +- Authorization Bypass Vulnerability - v1.14.1 [\#1242](https://github.com/schmittjoh/serializer/issues/1242) +- @SkipWhenEmpty and @Exclude combination leads to unexpected behavior [\#1240](https://github.com/schmittjoh/serializer/issues/1240) +- How to pass MetadataFactory::create\(\) into SerializationVisitorInterface::startVisitingObject\(\)? [\#1226](https://github.com/schmittjoh/serializer/issues/1226) +- Custom type in array key is not respected when serializing to JSON [\#1223](https://github.com/schmittjoh/serializer/issues/1223) +- xml:id or xml:lang attributes handling [\#1221](https://github.com/schmittjoh/serializer/issues/1221) +- Accessing static property as non static [\#1156](https://github.com/schmittjoh/serializer/issues/1156) +- AbstractVisitor::getElementType\(\) must be of the type array or null, string returned [\#1027](https://github.com/schmittjoh/serializer/issues/1027) **Merged pull requests:** -- Add psalm specific generic return type for deserialize [\#1091](https://github.com/schmittjoh/serializer/pull/1091) ([bdsl](https://github.com/bdsl)) -- Fix: Typo [\#1084](https://github.com/schmittjoh/serializer/pull/1084) ([localheinz](https://github.com/localheinz)) - -## [3.0.1](https://github.com/schmittjoh/serializer/tree/3.0.1) (2019-04-23) -**Fixed bugs:** - -- Do not throw exception when visiting null in custom handler [\#1076](https://github.com/schmittjoh/serializer/pull/1076) ([goetas](https://github.com/goetas)) - -## [3.0.0](https://github.com/schmittjoh/serializer/tree/3.0.0) (2019-04-23) - -**Backward incompatible changes:** +- remove missing deprecated removal on hasData [\#1245](https://github.com/schmittjoh/serializer/pull/1245) ([rflavien](https://github.com/rflavien)) +- Change return type of SerializerBuilder::build\(\) to Serializer [\#1241](https://github.com/schmittjoh/serializer/pull/1241) ([icanhazstring](https://github.com/icanhazstring)) +- docs: add note about array key type being ignored when serializing [\#1235](https://github.com/schmittjoh/serializer/pull/1235) ([eduardoweiland](https://github.com/eduardoweiland)) +- Sort packages in composer.json [\#1228](https://github.com/schmittjoh/serializer/pull/1228) ([simPod](https://github.com/simPod)) +- fix xml embeddable data getReference for DoctrineObjectConstructor [\#1224](https://github.com/schmittjoh/serializer/pull/1224) ([gam6itko](https://github.com/gam6itko)) +- fixed exception for strict\_types [\#1222](https://github.com/schmittjoh/serializer/pull/1222) ([ivoba](https://github.com/ivoba)) -- Revert v2 nested groups and release 3.0 [\#1071](https://github.com/schmittjoh/serializer/pull/1071) ([goetas](https://github.com/goetas)) +## [3.8.0](https://github.com/schmittjoh/serializer/tree/3.8.0) (2020-06-28) **Implemented enhancements:** -- use Twig 2.7 namespaces [\#1061](https://github.com/schmittjoh/serializer/pull/1061) ([IonBazan](https://github.com/IonBazan)) - -**Merged pull requests:** - -- Fix Travis-CI scripts always passing [\#1075](https://github.com/schmittjoh/serializer/pull/1075) ([IonBazan](https://github.com/IonBazan)) - -**Closed issues:** - -- \[RFC\] revert \#946 and release new major [\#1058](https://github.com/schmittjoh/serializer/issues/1058) +- Use doctrine/lexer instead of hoa/compiler [\#1212](https://github.com/schmittjoh/serializer/pull/1212) ([goetas](https://github.com/goetas)) -## [2.3.0](https://github.com/schmittjoh/serializer/tree/2.3.0) (2019-04-17) -**Implemented enhancements:** +**Fixed bugs:** -- add options property to XmlDeserializationVisitorFactory and XmlDeserializationVisitor, propagate defined value from factory to simplexml\_load\_string call [\#1068](https://github.com/schmittjoh/serializer/pull/1068) ([kopeckyales](https://github.com/kopeckyales)) +- Consider exclude rules on parents if defined [\#1206](https://github.com/schmittjoh/serializer/pull/1206) ([goetas](https://github.com/goetas)) **Closed issues:** -- Override existing property with another [\#1067](https://github.com/schmittjoh/serializer/issues/1067) -- disabling cdata by default [\#1065](https://github.com/schmittjoh/serializer/issues/1065) -- unwrap child class instance [\#1064](https://github.com/schmittjoh/serializer/issues/1064) -- Make JsonDeserializationVisitor extendable [\#1055](https://github.com/schmittjoh/serializer/issues/1055) +- Serializer Group [\#1213](https://github.com/schmittjoh/serializer/issues/1213) +- Notice: Accessing static property Proxies\\_\_CG\_\_\examplemodel\inherit\Customers::$lazyPropertiesNames as non static [\#1209](https://github.com/schmittjoh/serializer/issues/1209) +- Unserialization failure after upgrading to 3.7.0 \(`excludeIf` related?\) [\#1207](https://github.com/schmittjoh/serializer/issues/1207) +- \[RFC\] Removing abandoned hoa from serializer [\#1182](https://github.com/schmittjoh/serializer/issues/1182) +- hoa/protocol package conflicts with laravel helper [\#1154](https://github.com/schmittjoh/serializer/issues/1154) **Merged pull requests:** -- doc update: registerHandler\(\) example [\#1072](https://github.com/schmittjoh/serializer/pull/1072) ([cebe](https://github.com/cebe)) -- Updated suggestion for `JsonSerializationVisitor::addData` replacement [\#1066](https://github.com/schmittjoh/serializer/pull/1066) ([theoboldt](https://github.com/theoboldt)) -- Add fix to UPGRADING.md [\#1062](https://github.com/schmittjoh/serializer/pull/1062) ([Jean85](https://github.com/Jean85)) - +- Remove conflicts to hoa packages [\#1216](https://github.com/schmittjoh/serializer/pull/1216) ([alexander-schranz](https://github.com/alexander-schranz)) +- Test also agains twig 3 [\#1215](https://github.com/schmittjoh/serializer/pull/1215) ([alexander-schranz](https://github.com/alexander-schranz)) +- Allow doctrine/persistence v2/v3 [\#1210](https://github.com/schmittjoh/serializer/pull/1210) ([goetas](https://github.com/goetas)) +- Fix deprecated assertFileNotExist [\#1197](https://github.com/schmittjoh/serializer/pull/1197) ([mpoiriert](https://github.com/mpoiriert)) -## [2.2.0](https://github.com/schmittjoh/serializer/tree/2.2.0) +## [3.7.0](https://github.com/schmittjoh/serializer/tree/3.7.0) (2020-05-23) **Implemented enhancements:** -- Add Iterator Handler [\#1034](https://github.com/schmittjoh/serializer/pull/1034) ([scyzoryck](https://github.com/scyzoryck)) - +- Allow deserialization of typehinted DateTimeInterface to DateTime class [\#1193](https://github.com/schmittjoh/serializer/pull/1193) ([goetas](https://github.com/goetas)) +- Infer types from PHP 7.4 type declarations [\#1192](https://github.com/schmittjoh/serializer/pull/1192) ([goetas](https://github.com/goetas)) +- Support conditional exclude for classes [\#1099](https://github.com/schmittjoh/serializer/pull/1099) ([arneee](https://github.com/arneee)) **Fixed bugs:** -- xmlRootPrefix missing from unserialized metadata [\#1050](https://github.com/schmittjoh/serializer/issues/1050) -- Non-locale aware encoding of doubles, closes \#1041 [\#1042](https://github.com/schmittjoh/serializer/pull/1042) ([Grundik](https://github.com/Grundik)) +- Exclude if at class level are not merge [\#1203](https://github.com/schmittjoh/serializer/issues/1203) +- Class level expression exclusion strategy should work with hierarchies [\#1204](https://github.com/schmittjoh/serializer/pull/1204) ([goetas](https://github.com/goetas)) **Closed issues:** -- Using @Until and @Since on class level [\#1048](https://github.com/schmittjoh/serializer/issues/1048) -- Add use of annotation registry to docs [\#1044](https://github.com/schmittjoh/serializer/issues/1044) -- Values of type "double" should not use locale-specific encoding [\#1041](https://github.com/schmittjoh/serializer/issues/1041) -- Serialize Generator [\#1023](https://github.com/schmittjoh/serializer/issues/1023) +- Specify Type as nullable? [\#1191](https://github.com/schmittjoh/serializer/issues/1191) +- Does someone know how to use phpdoc with serializer? [\#1185](https://github.com/schmittjoh/serializer/issues/1185) +- Serializer doesn't keep types but convert them if not it can [\#1181](https://github.com/schmittjoh/serializer/issues/1181) +- ConditionalExpose/Exclude annotation does not work on class level [\#1098](https://github.com/schmittjoh/serializer/issues/1098) **Merged pull requests:** -- Test on php 7.3 [\#1054](https://github.com/schmittjoh/serializer/pull/1054) ([goetas](https://github.com/goetas)) -- xmlRootPrefix missing from unserialized metadata [\#1053](https://github.com/schmittjoh/serializer/pull/1053) ([goetas](https://github.com/goetas)) -- Allow @Since and @Until within @VirtualProperty on class level [\#1049](https://github.com/schmittjoh/serializer/pull/1049) ([tjveldhuizen](https://github.com/tjveldhuizen)) -- Document use of AnnotationRegistry [\#1047](https://github.com/schmittjoh/serializer/pull/1047) ([andig](https://github.com/andig)) -- Fix result of code example [\#1039](https://github.com/schmittjoh/serializer/pull/1039) ([henrikthesing](https://github.com/henrikthesing)) +- \[Docs\] Improve documentation on dynamic exclusion strategy [\#1188](https://github.com/schmittjoh/serializer/pull/1188) ([arneee](https://github.com/arneee)) +- Fix Support conditional exclude for classes [\#1187](https://github.com/schmittjoh/serializer/pull/1187) ([arneee](https://github.com/arneee)) +- Fix travis tests [\#1183](https://github.com/schmittjoh/serializer/pull/1183) ([peter279k](https://github.com/peter279k)) +- Replace "Exclude" by "Expose" [\#1180](https://github.com/schmittjoh/serializer/pull/1180) ([kpn13](https://github.com/kpn13)) +- add .gitattributes [\#1177](https://github.com/schmittjoh/serializer/pull/1177) ([Tobion](https://github.com/Tobion)) -## [2.1.0](https://github.com/schmittjoh/serializer/tree/2.1.0) +## [3.6.0](https://github.com/schmittjoh/serializer/tree/3.6.0) (2020-03-21) **Implemented enhancements:** -- Add compilable expression language [\#1010](https://github.com/schmittjoh/serializer/pull/1010) ([goetas](https://github.com/goetas)) - -**Closed issues:** - -- Compile error Declaration of \[...\] must be compatible with \[...\] [\#1024](https://github.com/schmittjoh/serializer/issues/1024) -- Exclude field for depth [\#1022](https://github.com/schmittjoh/serializer/issues/1022) -- Add class properties inheritance for extending classes \(e.g. XmlRoot\) [\#396](https://github.com/schmittjoh/serializer/issues/396) - -**Merged pull requests:** - -- fixed typo [\#1029](https://github.com/schmittjoh/serializer/pull/1029) ([sasezaki](https://github.com/sasezaki)) +- DateTime parsed invalid date [\#1152](https://github.com/schmittjoh/serializer/issues/1152) +- do not hide Exceptions from custom handlers but correctly handle null [\#1169](https://github.com/schmittjoh/serializer/pull/1169) ([Hikariii](https://github.com/Hikariii)) -## [2.0.2](https://github.com/schmittjoh/serializer/tree/2.0.2) (2018-12-12) **Fixed bugs:** -- jms serialzier 2.0 Error in debug mode [\#1018](https://github.com/schmittjoh/serializer/issues/1018) -- AbstractDoctrineTypeDriver::normalizeFieldType\(\) must be of the type string, null given [\#1015](https://github.com/schmittjoh/serializer/issues/1015) -- internal classes have false in reflection::getFilename\(\) [\#1013](https://github.com/schmittjoh/serializer/pull/1013) ([chregu](https://github.com/chregu)) +- Handle discriminator groups [\#1175](https://github.com/schmittjoh/serializer/pull/1175) ([goetas](https://github.com/goetas)) **Closed issues:** -- DateTime converted to ArrayObject instead of string in custom visitor class [\#1017](https://github.com/schmittjoh/serializer/issues/1017) +- thrown Exceptions are hidden when serializing complex objects with a handler [\#1168](https://github.com/schmittjoh/serializer/issues/1168) **Merged pull requests:** -- Doctrine driver normalizeFieldType method does not handle nulls [\#1020](https://github.com/schmittjoh/serializer/pull/1020) ([goetas](https://github.com/goetas)) -- fixed a typo [\#1014](https://github.com/schmittjoh/serializer/pull/1014) ([themasch](https://github.com/themasch)) - -## [2.0.1](https://github.com/schmittjoh/serializer/tree/2.0.1) (2018-11-29) -**Fixed bugs:** - -- BC Break on deserialize with non existing properties in JSON payload? [\#1011](https://github.com/schmittjoh/serializer/issues/1011) +- test serializing entity that uses Discriminator and extends some base… [\#1174](https://github.com/schmittjoh/serializer/pull/1174) ([FrKevin](https://github.com/FrKevin)) +- Handle ObjectConstructor returning NULL [\#1172](https://github.com/schmittjoh/serializer/pull/1172) ([jankramer](https://github.com/jankramer)) +- test symfony translator contract [\#1171](https://github.com/schmittjoh/serializer/pull/1171) ([goetas](https://github.com/goetas)) -**Merged pull requests:** - -- when a typed array is missing, do not try to de-serialize it [\#1012](https://github.com/schmittjoh/serializer/pull/1012) ([goetas](https://github.com/goetas)) -- Update UPGRADING.md [\#1008](https://github.com/schmittjoh/serializer/pull/1008) ([kunicmarko20](https://github.com/kunicmarko20)) +## [3.5.0](https://github.com/schmittjoh/serializer/tree/3.5.0) (2020-02-22) -## [2.0.0](https://github.com/schmittjoh/serializer/tree/2.0.0) (2018-11-09) **Implemented enhancements:** -- Move serialization info to the serialization context class [\#1006](https://github.com/schmittjoh/serializer/pull/1006) ([goetas](https://github.com/goetas)) -- Upgrade to Doctrine CS 5.0 [\#1002](https://github.com/schmittjoh/serializer/pull/1002) ([Majkl578](https://github.com/Majkl578)) -- Add travis build for docs [\#997](https://github.com/schmittjoh/serializer/pull/997) ([kunicmarko20](https://github.com/kunicmarko20)) +- Improved return type for fluent methods in Context [\#1162](https://github.com/schmittjoh/serializer/pull/1162) ([wouterj](https://github.com/wouterj)) +- Handle array format for dateHandler [\#1108](https://github.com/schmittjoh/serializer/pull/1108) ([VincentLanglet](https://github.com/VincentLanglet)) -**Merged pull requests:** - -- Added missing comma in 'Overriding Groups' example [\#1001](https://github.com/schmittjoh/serializer/pull/1001) ([skuhnow](https://github.com/skuhnow)) +**Fixed bugs:** -## [2.0.0-RC1](https://github.com/schmittjoh/serializer/tree/2.0.0-RC1) (2018-10-17) +- Make sure serialzation context is immutable [\#1159](https://github.com/schmittjoh/serializer/pull/1159) ([goetas](https://github.com/goetas)) **Merged pull requests:** -- Fixed typo in documentation \(stdclass.rst\) [\#998](https://github.com/schmittjoh/serializer/pull/998) ([moritzwachter](https://github.com/moritzwachter)) -- Improve handlers doc [\#996](https://github.com/schmittjoh/serializer/pull/996) ([kunicmarko20](https://github.com/kunicmarko20)) - -## [2.0.0-beta1](https://github.com/schmittjoh/serializer/tree/2.0.0-beta1) (2018-09-12) - -**Breaking changes:** - -- I want to change the default group used when overriding groups of deeper branches [\#898](https://github.com/schmittjoh/serializer/issues/898) -- NullAwareVisitorInterface::isNull second argument [\#823](https://github.com/schmittjoh/serializer/issues/823) -- Simplify deep group exclusion strategy [\#946](https://github.com/schmittjoh/serializer/pull/946) ([goetas](https://github.com/goetas)) -- Discriminator property serialization when parent is in discriminator map [\#879](https://github.com/schmittjoh/serializer/pull/879) ([supersmile2009](https://github.com/supersmile2009)) - -**Implemented enhancements:** - -- Change license to MIT [\#950](https://github.com/schmittjoh/serializer/issues/950) -- Do not instantiate visitors in the serialization builder [\#613](https://github.com/schmittjoh/serializer/issues/613) -- Possible to prefix the root element while serializing? [\#506](https://github.com/schmittjoh/serializer/issues/506) -- Add logo [\#976](https://github.com/schmittjoh/serializer/pull/976) ([goetas](https://github.com/goetas)) -- Implementation deserialization of Inline property [\#974](https://github.com/schmittjoh/serializer/pull/974) ([scyzoryck](https://github.com/scyzoryck)) -- Code style [\#971](https://github.com/schmittjoh/serializer/pull/971) ([goetas](https://github.com/goetas)) -- Make access strategies aware of the context [\#962](https://github.com/schmittjoh/serializer/pull/962) ([goetas](https://github.com/goetas)) -- Distinguish between metadata errors and run time errors [\#948](https://github.com/schmittjoh/serializer/pull/948) ([goetas](https://github.com/goetas)) -- Allow inline lists and maps [\#944](https://github.com/schmittjoh/serializer/pull/944) ([goetas](https://github.com/goetas)) -- Move property ordering strategy out of Metadata [\#938](https://github.com/schmittjoh/serializer/pull/938) ([Majkl578](https://github.com/Majkl578)) -- Do not use property metadata to get/set object values [\#934](https://github.com/schmittjoh/serializer/pull/934) ([goetas](https://github.com/goetas)) -- Type hints and final clases [\#930](https://github.com/schmittjoh/serializer/pull/930) ([goetas](https://github.com/goetas)) -- Added graph navigator factories [\#929](https://github.com/schmittjoh/serializer/pull/929) ([goetas](https://github.com/goetas)) -- No global graph navigator [\#925](https://github.com/schmittjoh/serializer/pull/925) ([goetas](https://github.com/goetas)) -- Allow instance of event filtering [\#924](https://github.com/schmittjoh/serializer/pull/924) ([goetas](https://github.com/goetas)) -- Move accessors to graph navigator [\#923](https://github.com/schmittjoh/serializer/pull/923) ([goetas](https://github.com/goetas)) -- Make PHPUnit stricter, drop redundant defaults [\#919](https://github.com/schmittjoh/serializer/pull/919) ([Majkl578](https://github.com/Majkl578)) -- Base exception should extend Throwable [\#911](https://github.com/schmittjoh/serializer/pull/911) ([Majkl578](https://github.com/Majkl578)) -- Port TypeParser to Hoa\Compiler [\#900](https://github.com/schmittjoh/serializer/pull/900) ([Majkl578](https://github.com/Majkl578)) -- \[2.0\] better handling when something gets excluded [\#895](https://github.com/schmittjoh/serializer/pull/895) ([goetas](https://github.com/goetas)) +- Allow for newer PHPUnit [\#1166](https://github.com/schmittjoh/serializer/pull/1166) ([sanmai](https://github.com/sanmai)) +- \[Docs\] Explain recursion in FileLocator [\#1155](https://github.com/schmittjoh/serializer/pull/1155) ([ruudk](https://github.com/ruudk)) +- Changed CI environment to stable PHP 7.4 [\#1153](https://github.com/schmittjoh/serializer/pull/1153) ([grogy](https://github.com/grogy)) -**Fixed bugs:** - -- XmlDeserializationVisitor references undefined property PropertyMetadata::$reflection [\#958](https://github.com/schmittjoh/serializer/issues/958) -- Invalid sprintf usage in DefaultAccessorStrategy [\#957](https://github.com/schmittjoh/serializer/issues/957) -- Serializer::handleDeserializeResult\(\) changes the result after using Visitor::visitArray\(\) [\#710](https://github.com/schmittjoh/serializer/issues/710) -- setGroups add new ExclusionStrategy instead of overwriting it [\#486](https://github.com/schmittjoh/serializer/issues/486) -- GraphNavigator using \LogicException without serializer namespace [\#473](https://github.com/schmittjoh/serializer/issues/473) -- Serialize doctrine entities load linked entities even if the max depth should stop this behavior [\#407](https://github.com/schmittjoh/serializer/issues/407) -- HandlerCallback inconsistent behaviour [\#324](https://github.com/schmittjoh/serializer/issues/324) -- Max Depth possible issue [\#272](https://github.com/schmittjoh/serializer/issues/272) -- MaxDepth shows empty array property [\#148](https://github.com/schmittjoh/serializer/issues/148) -- Custom Serialization handlers give an invalid result on top level [\#95](https://github.com/schmittjoh/serializer/issues/95) -- Deserialization to ArrayCollection not working as expected [\#9](https://github.com/schmittjoh/serializer/issues/9) -- Avoid duplicate exclusion strategies in the context [\#922](https://github.com/schmittjoh/serializer/pull/922) ([goetas](https://github.com/goetas)) +## [1.14.1](https://github.com/schmittjoh/serializer/tree/1.14.1) (2020-02-22) **Closed issues:** -- Revert fix for \#43 \(JSON representation of empty objects\) [\#942](https://github.com/schmittjoh/serializer/issues/942) -- Update jms/metadata [\#936](https://github.com/schmittjoh/serializer/issues/936) -- Property access should not rely on PropertyMetadata but only on AccessorStrategyInterface [\#932](https://github.com/schmittjoh/serializer/issues/932) -- Adopt some coding standard [\#914](https://github.com/schmittjoh/serializer/issues/914) -- Replace in-house EventDispatcher by an existing one [\#912](https://github.com/schmittjoh/serializer/issues/912) -- Risky tests in master [\#910](https://github.com/schmittjoh/serializer/issues/910) -- Move resolveMetadata from the GraphNavigator [\#906](https://github.com/schmittjoh/serializer/issues/906) -- Remove Context::getDirection\(\) [\#905](https://github.com/schmittjoh/serializer/issues/905) -- \[2.0 proposal\] Moving non-essential components to separate packages [\#902](https://github.com/schmittjoh/serializer/issues/902) -- \[2.0 proposal\] Splitting Navigator/Visitor [\#901](https://github.com/schmittjoh/serializer/issues/901) -- Virtual property is excluded when the name equals to an excluded property [\#896](https://github.com/schmittjoh/serializer/issues/896) -- Consider @var annotation for type inference when deserializing [\#893](https://github.com/schmittjoh/serializer/issues/893) -- Consider hoa/compiler [\#892](https://github.com/schmittjoh/serializer/issues/892) -- setSerialiseNull\(true\) + exclusion strategies still include data [\#852](https://github.com/schmittjoh/serializer/issues/852) -- Serializing a self-referencing object returns null [\#845](https://github.com/schmittjoh/serializer/issues/845) -- Allow generators as return type for SubscribingHandlerInterface::getSubscribingMethods [\#832](https://github.com/schmittjoh/serializer/issues/832) -- Add control to deserialization of null values [\#821](https://github.com/schmittjoh/serializer/issues/821) -- serialize null should be a boolean in the context [\#740](https://github.com/schmittjoh/serializer/issues/740) -- Allow data access to Property naming strategy [\#717](https://github.com/schmittjoh/serializer/issues/717) -- Custom handler works with array of objects but fails to serialize one object [\#700](https://github.com/schmittjoh/serializer/issues/700) -- Deprecate set\*ContextFactory in the Serializer to keep it immutable [\#691](https://github.com/schmittjoh/serializer/issues/691) -- Remove Symfony Validator \<2.6 support [\#687](https://github.com/schmittjoh/serializer/issues/687) -- Remove PHP metadata driver [\#686](https://github.com/schmittjoh/serializer/issues/686) -- Prevent doctrine proxy loading for virtual types by default [\#685](https://github.com/schmittjoh/serializer/issues/685) -- Inconsistency between serializing arrays and objects key names [\#655](https://github.com/schmittjoh/serializer/issues/655) -- Can't hint interface using @Type to trigger custom handler [\#631](https://github.com/schmittjoh/serializer/issues/631) -- Event Listeners are "lowercasing" class names for event match [\#624](https://github.com/schmittjoh/serializer/issues/624) -- JSON/YAML encoding changes [\#617](https://github.com/schmittjoh/serializer/issues/617) -- Remove PhpCollection and PhpOption and use arrays instead [\#616](https://github.com/schmittjoh/serializer/issues/616) -- Remove handler callback [\#615](https://github.com/schmittjoh/serializer/issues/615) -- MaxDepth exclusion strategy for OneToMany \(ArrayCollection\) type triggers too many doctrine queries. [\#500](https://github.com/schmittjoh/serializer/issues/500) -- Permit \(optional\) inheritance of HandlerCallback functions [\#499](https://github.com/schmittjoh/serializer/issues/499) -- Serialized name based on the format [\#456](https://github.com/schmittjoh/serializer/issues/456) -- obsolete strategies when calling Context::setVersion/setGroups more than once [\#98](https://github.com/schmittjoh/serializer/issues/98) +- Virtual Property do not get serialized if getter name conflict with a class property [\#1164](https://github.com/schmittjoh/serializer/issues/1164) +- SerializationGraphNavigator not receiving correct serializeNull config during initialize [\#1158](https://github.com/schmittjoh/serializer/issues/1158) +- SerializationGraphNavigator unaware of serializeNull change of context when altered in PreSerializeEvent [\#1157](https://github.com/schmittjoh/serializer/issues/1157) +- Memory leaks [\#1150](https://github.com/schmittjoh/serializer/issues/1150) +- Properties with @Groups annotations included in output when no SerializationContext given. [\#1149](https://github.com/schmittjoh/serializer/issues/1149) **Merged pull requests:** -- Fixed typo typo in UPGRADING.md [\#973](https://github.com/schmittjoh/serializer/pull/973) ([sweoggy](https://github.com/sweoggy)) -- Nullable exclusion strategy [\#965](https://github.com/schmittjoh/serializer/pull/965) ([goetas](https://github.com/goetas)) -- Revert https://github.com/schmittjoh/serializer/pull/113 [\#949](https://github.com/schmittjoh/serializer/pull/949) ([goetas](https://github.com/goetas)) -- This deprecates the set/has data functions in the json serialization [\#945](https://github.com/schmittjoh/serializer/pull/945) ([goetas](https://github.com/goetas)) -- Use jms/metadata 2.0 [\#940](https://github.com/schmittjoh/serializer/pull/940) ([goetas](https://github.com/goetas)) -- Short arrays, static asserts [\#927](https://github.com/schmittjoh/serializer/pull/927) ([Majkl578](https://github.com/Majkl578)) -- Move data handling from graph navigator to visitor [\#920](https://github.com/schmittjoh/serializer/pull/920) ([goetas](https://github.com/goetas)) -- Fix risky tests [\#918](https://github.com/schmittjoh/serializer/pull/918) ([goetas](https://github.com/goetas)) -- Enable strict mode, fix type juggling and drop some runtime coercion broken by design [\#916](https://github.com/schmittjoh/serializer/pull/916) ([Majkl578](https://github.com/Majkl578)) -- Upgrade PHPUnit [\#915](https://github.com/schmittjoh/serializer/pull/915) ([Majkl578](https://github.com/Majkl578)) -- Upcoming 2.0 [\#743](https://github.com/schmittjoh/serializer/pull/743) ([goetas](https://github.com/goetas)) -- Update configuration link to use reStructuredText link format [\#986](https://github.com/schmittjoh/serializer/pull/986) ([kinow](https://github.com/kinow)) -- Fix syntax for PHP code block [\#985](https://github.com/schmittjoh/serializer/pull/985) ([kinow](https://github.com/kinow)) -- Use reStructuredText link format [\#984](https://github.com/schmittjoh/serializer/pull/984) ([kinow](https://github.com/kinow)) +- PHP7.4 ternary operator deprecation [\#1163](https://github.com/schmittjoh/serializer/pull/1163) ([adhocore](https://github.com/adhocore)) +- Test 1.x on PHP 7.3 on Travis; fix builds for PHP 5.5 [\#1119](https://github.com/schmittjoh/serializer/pull/1119) ([sanmai](https://github.com/sanmai)) -## [1.13.0](https://github.com/schmittjoh/serializer/tree/1.13.0) (2018-07-25) +## [3.4.0](https://github.com/schmittjoh/serializer/tree/3.4.0) (2019-12-14) **Implemented enhancements:** -- Bugfix/metadata serialization [\#969](https://github.com/schmittjoh/serializer/pull/969) ([supersmile2009](https://github.com/supersmile2009)) - -**Fixed bugs:** - -- Exception on deserialization using XML and exclude-if [\#975](https://github.com/schmittjoh/serializer/issues/975) +- Symfony 5.0 compatibility [\#1145](https://github.com/schmittjoh/serializer/pull/1145) ([goetas](https://github.com/goetas)) +- Support new doctrine ODM proxy objects [\#1139](https://github.com/schmittjoh/serializer/pull/1139) ([notrix](https://github.com/notrix)) +- Visitor interfaces in handlers [\#1129](https://github.com/schmittjoh/serializer/pull/1129) ([derzkiy](https://github.com/derzkiy)) **Closed issues:** -- Serialization fails if root element has custom handler [\#961](https://github.com/schmittjoh/serializer/issues/961) -- Make inline property work with deserialization too [\#937](https://github.com/schmittjoh/serializer/issues/937) +- \[Improvement\] Ability to define a global exclusion\_policy: ALL for all classes. [\#1144](https://github.com/schmittjoh/serializer/issues/1144) +- Embed JSON string without extra escape [\#1142](https://github.com/schmittjoh/serializer/issues/1142) +- Make possible to set ArrayCollectionHandler classes from outside [\#1131](https://github.com/schmittjoh/serializer/issues/1131) **Merged pull requests:** -- Serializer 2.0 compatibility features [\#967](https://github.com/schmittjoh/serializer/pull/967) ([goetas](https://github.com/goetas)) +- Remove PHP 7.4 from `allow\_failures` matrix [\#1138](https://github.com/schmittjoh/serializer/pull/1138) ([carusogabriel](https://github.com/carusogabriel)) +- Remove unnecessary cast [\#1133](https://github.com/schmittjoh/serializer/pull/1133) ([carusogabriel](https://github.com/carusogabriel)) +- Fix PHPUnit deprecations [\#1123](https://github.com/schmittjoh/serializer/pull/1123) ([Majkl578](https://github.com/Majkl578)) -## [1.12.1](https://github.com/schmittjoh/serializer/tree/1.12.1) (2018-06-01) - -**Fixed bugs:** - -- Accessing static property as non static [\#960](https://github.com/schmittjoh/serializer/issues/960) -- creating JMS\Serializer\Metadata-\>closureAccessor on internal class failed [\#959](https://github.com/schmittjoh/serializer/issues/959) - -## [1.12.0](https://github.com/schmittjoh/serializer/tree/1.12.0) (2018-05-25) +## [3.3.0](https://github.com/schmittjoh/serializer/tree/3.3.0) (2019-09-20) **Implemented enhancements:** -- Add support for namespaced XML attribute on Discriminator + Tests [\#909](https://github.com/schmittjoh/serializer/pull/909) ([ArthurJam](https://github.com/ArthurJam)) -- Introduce graph navigator interface [\#876](https://github.com/schmittjoh/serializer/pull/876) ([goetas](https://github.com/goetas)) -- Use Bind closure accessor [\#875](https://github.com/schmittjoh/serializer/pull/875) ([goetas](https://github.com/goetas)) +- Update major version that v2.x deprecation will be removed [\#1134](https://github.com/schmittjoh/serializer/pull/1134) ([carusogabriel](https://github.com/carusogabriel)) +- Implement short expose syntax for XML as it is available for YAML [\#1127](https://github.com/schmittjoh/serializer/pull/1127) ([goetas](https://github.com/goetas)) **Fixed bugs:** -- DoctrineObjectConstructor and deserialize not work [\#806](https://github.com/schmittjoh/serializer/issues/806) -- \[Symfony\] DoctrineObjectorConstructor always creates new entity because of camel case to snake case conversion [\#734](https://github.com/schmittjoh/serializer/issues/734) -- Fix DoctrineObjectConstructor deserialization with naming strategies [\#951](https://github.com/schmittjoh/serializer/pull/951) ([re2bit](https://github.com/re2bit)) +- Avoid implicit expose of a property instead of virtual-property [\#1126](https://github.com/schmittjoh/serializer/pull/1126) ([goetas](https://github.com/goetas)) **Closed issues:** -- Feature proposal: dynamic property serialized name [\#225](https://github.com/schmittjoh/serializer/issues/225) -- Mapping request payload works for JSON but not for XML [\#820](https://github.com/schmittjoh/serializer/issues/820) +- Accessing static property as non static [\#1122](https://github.com/schmittjoh/serializer/issues/1122) +- Travis builds on 1.x are failing [\#1120](https://github.com/schmittjoh/serializer/issues/1120) **Merged pull requests:** -- Cange the spelling of a word [\#939](https://github.com/schmittjoh/serializer/pull/939) ([greg0ire](https://github.com/greg0ire)) -- Use dedicated PHPUnit assertions [\#928](https://github.com/schmittjoh/serializer/pull/928) ([carusogabriel](https://github.com/carusogabriel)) -- Update arrays.rst [\#907](https://github.com/schmittjoh/serializer/pull/907) ([burki](https://github.com/burki)) -- Change to MIT license [\#956](https://github.com/schmittjoh/serializer/pull/956) ([goetas](https://github.com/goetas)) -- Double logic for group exclusion \(20% faster\) [\#941](https://github.com/schmittjoh/serializer/pull/941) ([goetas](https://github.com/goetas)) -- Type casting tests [\#917](https://github.com/schmittjoh/serializer/pull/917) ([goetas](https://github.com/goetas)) -- Explicitly set serialization precision for tests [\#899](https://github.com/schmittjoh/serializer/pull/899) ([Majkl578](https://github.com/Majkl578)) -- Deprecations [\#877](https://github.com/schmittjoh/serializer/pull/877) ([goetas](https://github.com/goetas)) -- Added note on SerializedName annotation valididity [\#874](https://github.com/schmittjoh/serializer/pull/874) ([bobvandevijver](https://github.com/bobvandevijver)) -- Optimizations [\#861](https://github.com/schmittjoh/serializer/pull/861) ([goetas](https://github.com/goetas)) +- Allow failures on php "7.4snapshot" \(waiting for stable symfony 4.4\) [\#1128](https://github.com/schmittjoh/serializer/pull/1128) ([goetas](https://github.com/goetas)) -## [1.11.0](https://github.com/schmittjoh/serializer/tree/1.11.0) (2018-02-04) - -**Implemented enhancements:** - -- Deserialize xmlKeyValuePairs [\#868](https://github.com/schmittjoh/serializer/pull/868) ([goetas](https://github.com/goetas)) -- Add AdvancedNamingStrategyInterface [\#859](https://github.com/schmittjoh/serializer/pull/859) ([LeaklessGfy](https://github.com/LeaklessGfy)) -- Deserialize xmlKeyValuePairs [\#840](https://github.com/schmittjoh/serializer/pull/840) ([fdyckhoff](https://github.com/fdyckhoff)) +## [3.2.0](https://github.com/schmittjoh/serializer/tree/3.2.0) (2019-09-04) **Fixed bugs:** -- Exception thrown for non-existant accessor to an excluded property [\#862](https://github.com/schmittjoh/serializer/issues/862) -- Support non-namespaced lists in namespaced XML [\#851](https://github.com/schmittjoh/serializer/pull/851) ([bertterheide](https://github.com/bertterheide)) +- PHP7.4: Deprecated warning - serializationContext.php on line 152 [\#1111](https://github.com/schmittjoh/serializer/issues/1111) **Closed issues:** -- Context Group not working [\#865](https://github.com/schmittjoh/serializer/issues/865) -- Not all virtual properties are serialized [\#864](https://github.com/schmittjoh/serializer/issues/864) -- DeserializedName [\#857](https://github.com/schmittjoh/serializer/issues/857) -- Annotation does not exist, or could not be auto-loaded. [\#855](https://github.com/schmittjoh/serializer/issues/855) -- \[Question\] Serialization of primitive types [\#853](https://github.com/schmittjoh/serializer/issues/853) -- Empty list when deserializing namespaced XML with children that are not namespaced [\#850](https://github.com/schmittjoh/serializer/issues/850) -- XmlList\(skipWhenEmpty=true\) or @SkipWhenEmpty\(\) does not work [\#847](https://github.com/schmittjoh/serializer/issues/847) -- DateHandler Timezone ignored on deserialization [\#457](https://github.com/schmittjoh/serializer/issues/457) +- StaticPropertyMetadata first constructor argument not nullable [\#1116](https://github.com/schmittjoh/serializer/issues/1116) +- Add support for PSR-7 URIInterface objects [\#1115](https://github.com/schmittjoh/serializer/issues/1115) +- Upgraded 2.4 -\> 3.4 / Symfony 4.3.3 [\#1112](https://github.com/schmittjoh/serializer/issues/1112) +- Empty namespace [\#1087](https://github.com/schmittjoh/serializer/issues/1087) +- Format constants \(JSON, XML\) [\#1079](https://github.com/schmittjoh/serializer/issues/1079) +- @ExclusionPolicy\(policy="ALL"\) causes PHP notice message [\#1073](https://github.com/schmittjoh/serializer/issues/1073) **Merged pull requests:** -- Drop HHVM support [\#869](https://github.com/schmittjoh/serializer/pull/869) ([goetas](https://github.com/goetas)) -- Allow excluded private properties to not have a getter acc… [\#863](https://github.com/schmittjoh/serializer/pull/863) ([0mars](https://github.com/0mars)) -- Solve php 7.2 deprecations [\#860](https://github.com/schmittjoh/serializer/pull/860) ([goetas](https://github.com/goetas)) -- Fixed issue where timezone is lost when creating DateTime from unix timestamp [\#835](https://github.com/schmittjoh/serializer/pull/835) ([goetas](https://github.com/goetas)) +- Explain once and for all the use of StaticPropertyMetadata [\#1118](https://github.com/schmittjoh/serializer/pull/1118) ([goetas](https://github.com/goetas)) +- PHP 7.4 compatibility [\#1113](https://github.com/schmittjoh/serializer/pull/1113) ([goetas](https://github.com/goetas)) +- Fix typos in UPGRADING.md [\#1107](https://github.com/schmittjoh/serializer/pull/1107) ([jdreesen](https://github.com/jdreesen)) +- Fix exclusion policy bug [\#1106](https://github.com/schmittjoh/serializer/pull/1106) ([spam312sn](https://github.com/spam312sn)) +- Add Doctrine 2 immutable datetime types to field mapping. [\#1104](https://github.com/schmittjoh/serializer/pull/1104) ([Sonny812](https://github.com/Sonny812)) -## [1.10.0](https://github.com/schmittjoh/serializer/tree/1.10.0) (2017-11-30) +## [3.1.1](https://github.com/schmittjoh/serializer/tree/3.1.1) (2019-06-28) -**Implemented enhancements:** +**Fixed bugs:** -- support PSR-11 compatible DI containers [\#844](https://github.com/schmittjoh/serializer/pull/844) ([xabbuh](https://github.com/xabbuh)) +- Could not deserialize object if all properties have not type [\#1102](https://github.com/schmittjoh/serializer/issues/1102) +- Revert "Move type check when deserializing into the graph navigator" [\#1103](https://github.com/schmittjoh/serializer/pull/1103) ([goetas](https://github.com/goetas)) -**Closed issues:** +## [3.1.0](https://github.com/schmittjoh/serializer/tree/3.1.0) (2019-06-25) -- Serialize using jsonSerialize\(\) if object implements JsonSerializable [\#846](https://github.com/schmittjoh/serializer/issues/846) -- ExclusionStrategy backward compatibility break [\#843](https://github.com/schmittjoh/serializer/issues/843) -- @MaxDepth jms/serializer-bundle 2.2 [\#842](https://github.com/schmittjoh/serializer/issues/842) +**Implemented enhancements:** -## [1.9.2](https://github.com/schmittjoh/serializer/tree/1.9.2) (2017-11-22) +- Add support for iterable and Iterator [\#1096](https://github.com/schmittjoh/serializer/pull/1096) ([simPod](https://github.com/simPod)) +- Implement "empty" XML namespace handling [\#1095](https://github.com/schmittjoh/serializer/pull/1095) ([discordier](https://github.com/discordier)) +- Move type check when deserializing into the graph navigator [\#1080](https://github.com/schmittjoh/serializer/pull/1080) ([goetas](https://github.com/goetas)) +- Allow loading different YAML extensions [\#1078](https://github.com/schmittjoh/serializer/pull/1078) ([scaytrase](https://github.com/scaytrase)) **Fixed bugs:** -- Missing ClassMetadata deserialization data [\#841](https://github.com/schmittjoh/serializer/pull/841) ([TristanMogwai](https://github.com/TristanMogwai)) +- Fix for failing doctrine object constructor on embeddable class [\#1031](https://github.com/schmittjoh/serializer/pull/1031) ([notrix](https://github.com/notrix)) **Closed issues:** -- DateTime format documentation [\#836](https://github.com/schmittjoh/serializer/issues/836) -- Deserialization not working with camelCase [\#831](https://github.com/schmittjoh/serializer/issues/831) +- Behavior serializeNull -\> not always honored in 2.\* \(but was in 1.\*\) [\#1101](https://github.com/schmittjoh/serializer/issues/1101) +- Support for iterable [\#1094](https://github.com/schmittjoh/serializer/issues/1094) +- Prevent deserialisation with missing required field [\#1090](https://github.com/schmittjoh/serializer/issues/1090) +- Allow using @XmlValue together with @Accessor/@AccessType [\#1083](https://github.com/schmittjoh/serializer/issues/1083) +- Support \*.yaml extension [\#1077](https://github.com/schmittjoh/serializer/issues/1077) +- Instructions for upgrading from addData in 1.x don't work [\#1030](https://github.com/schmittjoh/serializer/issues/1030) **Merged pull requests:** -- Fix documentation syntax errors on available types [\#839](https://github.com/schmittjoh/serializer/pull/839) ([andy-morgan](https://github.com/andy-morgan)) -- Improve documentation about default DateTime format [\#838](https://github.com/schmittjoh/serializer/pull/838) ([enumag](https://github.com/enumag)) +- Add psalm specific generic return type for deserialize [\#1091](https://github.com/schmittjoh/serializer/pull/1091) ([bdsl](https://github.com/bdsl)) +- Fix: Typo [\#1084](https://github.com/schmittjoh/serializer/pull/1084) ([localheinz](https://github.com/localheinz)) -## [1.9.1](https://github.com/schmittjoh/serializer/tree/1.9.1) (2017-10-27) +## [3.0.1](https://github.com/schmittjoh/serializer/tree/3.0.1) (2019-04-23) **Fixed bugs:** -- Dynamic exclusion strategy, Variable "object" is not valid [\#826](https://github.com/schmittjoh/serializer/issues/826) - -**Closed issues:** - -- Allow DateTime or Null [\#779](https://github.com/schmittjoh/serializer/issues/779) +- Do not throw exception when visiting null in custom handler [\#1076](https://github.com/schmittjoh/serializer/pull/1076) ([goetas](https://github.com/goetas)) -**Merged pull requests:** +## [3.0.0](https://github.com/schmittjoh/serializer/tree/3.0.0) (2019-04-23) -- Alow to use "object" var in expressions when deserializing [\#827](https://github.com/schmittjoh/serializer/pull/827) ([goetas](https://github.com/goetas)) +**Breaking changes:** -## [1.9.0](https://github.com/schmittjoh/serializer/tree/1.9.0) (2017-09-28) +- Revert v2 nested groups and release 3.0 [\#1071](https://github.com/schmittjoh/serializer/pull/1071) ([goetas](https://github.com/goetas)) **Implemented enhancements:** -- Doctrine LazyCriteriaCollection not supported [\#814](https://github.com/schmittjoh/serializer/issues/814) -- Do not require the translator [\#824](https://github.com/schmittjoh/serializer/pull/824) ([goetas](https://github.com/goetas)) -- Added mapping for guid type [\#802](https://github.com/schmittjoh/serializer/pull/802) ([develth](https://github.com/develth)) -- Added translation domain to FormErrorHandler [\#783](https://github.com/schmittjoh/serializer/pull/783) ([prosalov](https://github.com/prosalov)) - -**Fixed bugs:** - -- Node no longer exists - Deserialize Error [\#817](https://github.com/schmittjoh/serializer/issues/817) -- Serializer fails if there is no AnnotationDriver in the DriverChain instance [\#815](https://github.com/schmittjoh/serializer/issues/815) -- Evaluate XML xsi:nil="1" to null [\#799](https://github.com/schmittjoh/serializer/pull/799) ([Bouwdie](https://github.com/Bouwdie)) +- use Twig 2.7 namespaces [\#1061](https://github.com/schmittjoh/serializer/pull/1061) ([IonBazan](https://github.com/IonBazan)) **Closed issues:** -- Empty array removed from XML serialization [\#816](https://github.com/schmittjoh/serializer/issues/816) -- XML Discriminator tags don't work in YAML metadata [\#811](https://github.com/schmittjoh/serializer/issues/811) -- Launching phpunit does not execute any test [\#809](https://github.com/schmittjoh/serializer/issues/809) -- Add "bool" Annotation/Type [\#807](https://github.com/schmittjoh/serializer/issues/807) -- Add support for overriding default annotation driver configuration [\#804](https://github.com/schmittjoh/serializer/issues/804) -- Add description to PropertyMetadata? [\#800](https://github.com/schmittjoh/serializer/issues/800) +- \[RFC\] revert \#946 and release new major [\#1058](https://github.com/schmittjoh/serializer/issues/1058) **Merged pull requests:** -- Workaround to avoid triggering simplexml warning [\#825](https://github.com/schmittjoh/serializer/pull/825) ([goetas](https://github.com/goetas)) -- Added null metadata driver [\#822](https://github.com/schmittjoh/serializer/pull/822) ([goetas](https://github.com/goetas)) -- Run Travis tests against modern PHP [\#819](https://github.com/schmittjoh/serializer/pull/819) ([Majkl578](https://github.com/Majkl578)) -- Added bool type alias [\#818](https://github.com/schmittjoh/serializer/pull/818) ([Majkl578](https://github.com/Majkl578)) -- Revert back to PSR-0 [\#797](https://github.com/schmittjoh/serializer/pull/797) ([goetas](https://github.com/goetas)) - -## [1.8.1](https://github.com/schmittjoh/serializer/tree/1.8.1) (2017-07-13) - -**Closed issues:** +- Fix Travis-CI scripts always passing [\#1075](https://github.com/schmittjoh/serializer/pull/1075) ([IonBazan](https://github.com/IonBazan)) -- Version 1.8 is breaking backwards compatibility [\#796](https://github.com/schmittjoh/serializer/issues/796) +## [1.14.0](https://github.com/schmittjoh/serializer/tree/1.14.0) (2019-04-17) -## [1.8.0](https://github.com/schmittjoh/serializer/tree/1.8.0) (2017-07-12) +## [2.3.0](https://github.com/schmittjoh/serializer/tree/2.3.0) (2019-04-17) **Implemented enhancements:** -- Detect XML xsi:nil="true" to null when deserializing [\#790](https://github.com/schmittjoh/serializer/pull/790) ([goetas](https://github.com/goetas)) -- Added support for a third deserialize parameter for the DateTime type [\#788](https://github.com/schmittjoh/serializer/pull/788) ([bobvandevijver](https://github.com/bobvandevijver)) -- Added trim to xml metadata reader for groups parameter, and added support for groups element [\#781](https://github.com/schmittjoh/serializer/pull/781) ([mrosiu](https://github.com/mrosiu)) -- Add propertyMetdata to dynamic expression variables [\#778](https://github.com/schmittjoh/serializer/pull/778) ([goetas](https://github.com/goetas)) -- Fix xml deserialization when xsi:nil="true" is set [\#771](https://github.com/schmittjoh/serializer/pull/771) ([Bouwdie](https://github.com/Bouwdie)) - -**Fixed bugs:** - -- do not disappear type params in DoctrineProxySubscriber [\#793](https://github.com/schmittjoh/serializer/pull/793) ([kriswallsmith](https://github.com/kriswallsmith)) -- \#784 fix with inline array of type array\ [\#785](https://github.com/schmittjoh/serializer/pull/785) ([aviortm](https://github.com/aviortm)) +- Expose and test GroupsExclusionStrategy::getGroupsFor\(\) [\#1069](https://github.com/schmittjoh/serializer/pull/1069) ([goetas](https://github.com/goetas)) +- add options property to XmlDeserializationVisitorFactory and XmlDeserializationVisitor, propagate defined value from factory to simplexml\_load\_string call [\#1068](https://github.com/schmittjoh/serializer/pull/1068) ([kopeckyales](https://github.com/kopeckyales)) **Closed issues:** -- inline array with type array\ not serialized [\#784](https://github.com/schmittjoh/serializer/issues/784) -- \[2.0\] \[Feature-request\] Provide InitializedObjectConstructor as default [\#775](https://github.com/schmittjoh/serializer/issues/775) -- Allow access to PropertyMetadata in Dynamic Exclusion strategies [\#772](https://github.com/schmittjoh/serializer/issues/772) -- Overriding groups at runtime does not work, or? [\#767](https://github.com/schmittjoh/serializer/issues/767) -- DateTime format and control characters [\#94](https://github.com/schmittjoh/serializer/issues/94) +- Override existing property with another [\#1067](https://github.com/schmittjoh/serializer/issues/1067) +- disabling cdata by default [\#1065](https://github.com/schmittjoh/serializer/issues/1065) +- unwrap child class instance [\#1064](https://github.com/schmittjoh/serializer/issues/1064) +- Make JsonDeserializationVisitor extendable [\#1055](https://github.com/schmittjoh/serializer/issues/1055) **Merged pull requests:** -- Missing features of the compiler pass [\#789](https://github.com/schmittjoh/serializer/pull/789) ([mikemix](https://github.com/mikemix)) -- Updated documentation related to PR \#778 [\#780](https://github.com/schmittjoh/serializer/pull/780) ([bblue](https://github.com/bblue)) -- \[RFC\] Move to PSR 4 [\#770](https://github.com/schmittjoh/serializer/pull/770) ([goetas](https://github.com/goetas)) -- Re-formatted code for better PSR compliance [\#769](https://github.com/schmittjoh/serializer/pull/769) ([goetas](https://github.com/goetas)) -- Proposing some guidelines for contributing [\#763](https://github.com/schmittjoh/serializer/pull/763) ([goetas](https://github.com/goetas)) - -## [1.7.1](https://github.com/schmittjoh/serializer/tree/1.7.1) (2017-05-15) - -**Fixed bugs:** - -- Custom type handlers does not work with doctrine proxies anymore [\#765](https://github.com/schmittjoh/serializer/issues/765) -- Doctrine listener should not change the type on proxies with virtual type [\#768](https://github.com/schmittjoh/serializer/pull/768) ([goetas](https://github.com/goetas)) - -**Closed issues:** - -- Missing bool type in graphNavigator.php in method accept\(\) [\#764](https://github.com/schmittjoh/serializer/issues/764) -- The sub-class "Proxy-Class" is not listed in the discriminator of the base class "DiscriminatorClass" [\#459](https://github.com/schmittjoh/serializer/issues/459) -- Configure whether serializing empty array. [\#124](https://github.com/schmittjoh/serializer/issues/124) - -## [1.7.0](https://github.com/schmittjoh/serializer/tree/1.7.0) (2017-05-10) - -**Implemented enhancements:** - -- Skip doctrine proxy initializations when exclusion strategy will exclude it [\#760](https://github.com/schmittjoh/serializer/pull/760) ([goetas](https://github.com/goetas)) - -**Closed issues:** - -- Error deserializing a map of \(nullable\) objects [\#762](https://github.com/schmittjoh/serializer/issues/762) -- Add data using setData produces hashes instead of arrays [\#761](https://github.com/schmittjoh/serializer/issues/761) +- doc update: registerHandler\(\) example [\#1072](https://github.com/schmittjoh/serializer/pull/1072) ([cebe](https://github.com/cebe)) +- Updated suggestion for `JsonSerializationVisitor::addData` replacement [\#1066](https://github.com/schmittjoh/serializer/pull/1066) ([theoboldt](https://github.com/theoboldt)) +- Add fix to UPGRADING.md [\#1062](https://github.com/schmittjoh/serializer/pull/1062) ([Jean85](https://github.com/Jean85)) -## [1.7.0-RC2](https://github.com/schmittjoh/serializer/tree/1.7.0-RC2) (2017-05-05) +## [2.2.0](https://github.com/schmittjoh/serializer/tree/2.2.0) (2019-02-27) **Implemented enhancements:** -- Make sure input is always a string [\#755](https://github.com/schmittjoh/serializer/pull/755) ([goetas](https://github.com/goetas)) -- Allow namespaced XML element discriminator [\#753](https://github.com/schmittjoh/serializer/pull/753) ([goetas](https://github.com/goetas)) +- Add Iterator Handler [\#1034](https://github.com/schmittjoh/serializer/pull/1034) ([scyzoryck](https://github.com/scyzoryck)) **Fixed bugs:** -- Allow to skip "empty serialization result" when serializing [\#757](https://github.com/schmittjoh/serializer/pull/757) ([goetas](https://github.com/goetas)) +- xmlRootPrefix missing from unserialized metadata [\#1050](https://github.com/schmittjoh/serializer/issues/1050) +- Non-locale aware encoding of doubles, closes \#1041 [\#1042](https://github.com/schmittjoh/serializer/pull/1042) ([Grundik](https://github.com/Grundik)) **Closed issues:** -- Is it possible to use @XmlNamespace & @XmlRoot in a class at same time ? [\#759](https://github.com/schmittjoh/serializer/issues/759) -- Serializes FOS:User datas with ExclusionPolicy\("all"\) [\#599](https://github.com/schmittjoh/serializer/issues/599) +- GROUP BY [\#1051](https://github.com/schmittjoh/serializer/issues/1051) +- Using @Until and @Since on class level [\#1048](https://github.com/schmittjoh/serializer/issues/1048) +- \[Semantical Error\] The annotation \"@generated\" in class JMS\\Serializer\\Type\\InnerParser was never imported [\#1046](https://github.com/schmittjoh/serializer/issues/1046) +- ReflectionException when \(de\)serializing unless fully qualified classname is used [\#1045](https://github.com/schmittjoh/serializer/issues/1045) +- Add use of annotation registry to docs [\#1044](https://github.com/schmittjoh/serializer/issues/1044) +- Values of type "double" should not use locale-specific encoding [\#1041](https://github.com/schmittjoh/serializer/issues/1041) +- SF4: JMS serializer seems to be ignoring global naming strategy [\#1037](https://github.com/schmittjoh/serializer/issues/1037) +- @SerializedName not being ignored since 2.x is bug or feature? [\#1036](https://github.com/schmittjoh/serializer/issues/1036) +- What should I use instead of the dropped GenericDeserializationVisitor class? [\#1035](https://github.com/schmittjoh/serializer/issues/1035) +- DateTime and DateTimeImmutable from PHP 7.1 serialization and deserialization with microseconds [\#1033](https://github.com/schmittjoh/serializer/issues/1033) +- Provide an option to the SerializeBuilder to set AccessType to a specified value globally [\#1025](https://github.com/schmittjoh/serializer/issues/1025) +- Serialize Generator [\#1023](https://github.com/schmittjoh/serializer/issues/1023) **Merged pull requests:** -- Add a quick reference for how to enable expression evaluator [\#758](https://github.com/schmittjoh/serializer/pull/758) ([chasen](https://github.com/chasen)) -- Allow for setExpressionEvaluator usage to be chainable [\#756](https://github.com/schmittjoh/serializer/pull/756) ([chasen](https://github.com/chasen)) -- Fix typo in annotation docs [\#754](https://github.com/schmittjoh/serializer/pull/754) ([JustBlackBird](https://github.com/JustBlackBird)) - -## [1.7.0-RC1](https://github.com/schmittjoh/serializer/tree/1.7.0-RC1) (2017-04-25) - -**Implemented enhancements:** - -- Allow to configure the doctrine object constructor [\#751](https://github.com/schmittjoh/serializer/pull/751) ([goetas](https://github.com/goetas)) -- Trigger doctrine events on doctrine proxies [\#750](https://github.com/schmittjoh/serializer/pull/750) ([goetas](https://github.com/goetas)) -- Added stdClass serialization handler [\#749](https://github.com/schmittjoh/serializer/pull/749) ([goetas](https://github.com/goetas)) - -**Fixed bugs:** +- Test on php 7.3 [\#1054](https://github.com/schmittjoh/serializer/pull/1054) ([goetas](https://github.com/goetas)) +- xmlRootPrefix missing from unserialized metadata [\#1053](https://github.com/schmittjoh/serializer/pull/1053) ([goetas](https://github.com/goetas)) +- Allow @Since and @Until within @VirtualProperty on class level [\#1049](https://github.com/schmittjoh/serializer/pull/1049) ([tjveldhuizen](https://github.com/tjveldhuizen)) +- Document use of AnnotationRegistry [\#1047](https://github.com/schmittjoh/serializer/pull/1047) ([andig](https://github.com/andig)) +- Fix result of code example [\#1039](https://github.com/schmittjoh/serializer/pull/1039) ([henrikthesing](https://github.com/henrikthesing)) -- Array gets serialized as object, not as array, depending on order. [\#709](https://github.com/schmittjoh/serializer/issues/709) -- Doctrine Proxies and serializer.pre\_serialize [\#666](https://github.com/schmittjoh/serializer/issues/666) -- Fix stdClass inconsistencies when serializing to JSON [\#730](https://github.com/schmittjoh/serializer/pull/730) ([goetas](https://github.com/goetas)) -- Allow to typehint for the type \(array/hash\) of the root item to be serialized [\#728](https://github.com/schmittjoh/serializer/pull/728) ([goetas](https://github.com/goetas)) +## [2.1.0](https://github.com/schmittjoh/serializer/tree/2.1.0) (2019-01-11) **Closed issues:** -- Array serialized as JSON object [\#706](https://github.com/schmittjoh/serializer/issues/706) -- From old issue \#290 [\#670](https://github.com/schmittjoh/serializer/issues/670) -- Form validation error response - field names not converted from camel case to underscore [\#587](https://github.com/schmittjoh/serializer/issues/587) -- Ability to getGroups on Context [\#554](https://github.com/schmittjoh/serializer/issues/554) -- SerializedName misleading usage and constructor issue [\#548](https://github.com/schmittjoh/serializer/issues/548) -- Discriminator should support xmlAttribute [\#547](https://github.com/schmittjoh/serializer/issues/547) -- Public method accessor is required for excluded/not exposed properties [\#519](https://github.com/schmittjoh/serializer/issues/519) -- Entity changed via preserialize and wrongly persisted [\#509](https://github.com/schmittjoh/serializer/issues/509) -- XML deserialization properties null when using default namespace [\#504](https://github.com/schmittjoh/serializer/issues/504) -- AccessorOrder is ignored [\#501](https://github.com/schmittjoh/serializer/issues/501) -- Deserialization of sub entites with non existing id [\#492](https://github.com/schmittjoh/serializer/issues/492) -- \[Question\] Handler/Converter for specific field [\#476](https://github.com/schmittjoh/serializer/issues/476) -- getClassName regex may incorrectly retrieve a false class name from comments above the class. [\#460](https://github.com/schmittjoh/serializer/issues/460) -- Multiple types for property? [\#445](https://github.com/schmittjoh/serializer/issues/445) -- Allow non-qualified XML serialization when XML namespaces are part of the metadata [\#413](https://github.com/schmittjoh/serializer/issues/413) -- Discriminator field name [\#412](https://github.com/schmittjoh/serializer/issues/412) -- Serializing to and deserializing from DateTime is inconsistent [\#394](https://github.com/schmittjoh/serializer/issues/394) -- ManyToOne and OneToMany Serialization Groups [\#387](https://github.com/schmittjoh/serializer/issues/387) -- Static SubscribingHandlerInterface::getSubscribingMethod [\#380](https://github.com/schmittjoh/serializer/issues/380) -- User defined ordering function [\#379](https://github.com/schmittjoh/serializer/issues/379) -- serialized\_name for discriminator [\#372](https://github.com/schmittjoh/serializer/issues/372) -- Serializing object with empty array [\#350](https://github.com/schmittjoh/serializer/issues/350) -- VirtualProperty\(s\) are ignored with AccessorOrder [\#349](https://github.com/schmittjoh/serializer/issues/349) -- When setting a group of serialization, the inheritance doesn't work anymore [\#328](https://github.com/schmittjoh/serializer/issues/328) -- Serialization of empty object [\#323](https://github.com/schmittjoh/serializer/issues/323) -- "Can't pop from an empty datastructure" error when multiple serializer calls [\#319](https://github.com/schmittjoh/serializer/issues/319) -- virtual\_properties cannot be excluded with groups [\#291](https://github.com/schmittjoh/serializer/issues/291) -- Integer serialized as String using VirtualProperty [\#289](https://github.com/schmittjoh/serializer/issues/289) -- SimpleObjectProxy is not implement abstract methods of Proxy class [\#287](https://github.com/schmittjoh/serializer/issues/287) -- Serializing array that have one of the element or member of an element an empty object [\#277](https://github.com/schmittjoh/serializer/issues/277) -- Serialization with groups return json object instead array [\#267](https://github.com/schmittjoh/serializer/issues/267) -- The purpose of "Force JSON output to "{}" instead of "\[\]" if it contains either no properties or all properties are null" [\#248](https://github.com/schmittjoh/serializer/issues/248) -- Json array serialisation [\#242](https://github.com/schmittjoh/serializer/issues/242) -- Ignoring "Assert" in output doc if excluded [\#241](https://github.com/schmittjoh/serializer/issues/241) -- Alphabetical accessor order doesn't respect SerializedName overrides [\#240](https://github.com/schmittjoh/serializer/issues/240) -- Request Annotation for Array Data [\#234](https://github.com/schmittjoh/serializer/issues/234) -- Allow @var instead of @Type when deserializing [\#233](https://github.com/schmittjoh/serializer/issues/233) -- Strange issue with groups exclusion strategy [\#230](https://github.com/schmittjoh/serializer/issues/230) -- Warning when deserializing removed entity [\#216](https://github.com/schmittjoh/serializer/issues/216) -- Where in the JMS code does the navigator call VisitProperty method for visitor [\#207](https://github.com/schmittjoh/serializer/issues/207) -- Property of the type array is not in alphabetic order after serialization [\#196](https://github.com/schmittjoh/serializer/issues/196) -- Magic and inconsistencies in array serialization [\#191](https://github.com/schmittjoh/serializer/issues/191) -- PreSerialization Event not handled if the value is not object [\#162](https://github.com/schmittjoh/serializer/issues/162) -- Hierarchical object serialization does not appear to inherit metadata from ancestors for metadata defined in XML [\#151](https://github.com/schmittjoh/serializer/issues/151) -- When using MaxDepth, Serialization of an array entitiy is not working [\#132](https://github.com/schmittjoh/serializer/issues/132) -- Switch to change default naming strategy [\#128](https://github.com/schmittjoh/serializer/issues/128) -- Throw exceptions on invalid input [\#112](https://github.com/schmittjoh/serializer/issues/112) -- Recursion detected error when serialization groups are in use [\#96](https://github.com/schmittjoh/serializer/issues/96) -- Allow serialization groups to be accessible within event subscriber callbacks. [\#84](https://github.com/schmittjoh/serializer/issues/84) -- Allow Constructed Object to be Passed to Deserialize [\#79](https://github.com/schmittjoh/serializer/issues/79) -- JSON recursion when first object in root list is empty [\#61](https://github.com/schmittjoh/serializer/issues/61) -- Can't serialize an array with an empty object [\#59](https://github.com/schmittjoh/serializer/issues/59) +- Compile error Declaration of \[...\] must be compatible with \[...\] [\#1024](https://github.com/schmittjoh/serializer/issues/1024) +- Exclude field for depth [\#1022](https://github.com/schmittjoh/serializer/issues/1022) **Merged pull requests:** -- Added runtime twig extension support \(significant performance improvements\) [\#747](https://github.com/schmittjoh/serializer/pull/747) ([goetas](https://github.com/goetas)) - -## [1.6.2](https://github.com/schmittjoh/serializer/tree/1.6.2) (2017-04-17) - -**Fixed bugs:** - -- @VirtualProperty "exp" does not play nice with @ExclusionPolicy\("ALL"\) [\#746](https://github.com/schmittjoh/serializer/issues/746) +- fixed typo [\#1029](https://github.com/schmittjoh/serializer/pull/1029) ([sasezaki](https://github.com/sasezaki)) -## [1.6.1](https://github.com/schmittjoh/serializer/tree/1.6.1) (2017-04-12) +## [2.0.2](https://github.com/schmittjoh/serializer/tree/2.0.2) (2018-12-12) **Fixed bugs:** -- Do not output the XML node when the object will be emtpy [\#744](https://github.com/schmittjoh/serializer/pull/744) ([goetas](https://github.com/goetas)) +- jms serialzier 2.0 Error in debug mode [\#1018](https://github.com/schmittjoh/serializer/issues/1018) +- AbstractDoctrineTypeDriver::normalizeFieldType\(\) must be of the type string, null given [\#1015](https://github.com/schmittjoh/serializer/issues/1015) +- allow empty strings and numbers as metadata type parameters [\#1019](https://github.com/schmittjoh/serializer/pull/1019) ([goetas](https://github.com/goetas)) +- internal classes have false in reflection::getFilename\(\) [\#1013](https://github.com/schmittjoh/serializer/pull/1013) ([chregu](https://github.com/chregu)) **Closed issues:** -- XmlList not working since version 1.5.0 with xmlns attributes [\#742](https://github.com/schmittjoh/serializer/issues/742) -- DoctrineObjectConstructor: how to use it without Symfony, in a PHP project [\#741](https://github.com/schmittjoh/serializer/issues/741) -- Outdated docs site [\#733](https://github.com/schmittjoh/serializer/issues/733) -- Why do we need this check inside SerializedName constructor, if there is name? [\#558](https://github.com/schmittjoh/serializer/issues/558) -- Is it possible to deserialize Collection from Json [\#534](https://github.com/schmittjoh/serializer/issues/534) -- PhpCollection 0.4 [\#531](https://github.com/schmittjoh/serializer/issues/531) -- Possible mismatch of xml-attribute-map and $pMetadata-\>xmlAttribute in XmlDriver.php [\#422](https://github.com/schmittjoh/serializer/issues/422) -- Access level propose for Handler/DateHandler.php [\#386](https://github.com/schmittjoh/serializer/issues/386) -- Type DateTime and Timestamp \(U format\) [\#343](https://github.com/schmittjoh/serializer/issues/343) +- DateTime converted to ArrayObject instead of string in custom visitor class [\#1017](https://github.com/schmittjoh/serializer/issues/1017) **Merged pull requests:** -- Update PHPDocs [\#736](https://github.com/schmittjoh/serializer/pull/736) ([gnat42](https://github.com/gnat42)) - -## [1.6.0](https://github.com/schmittjoh/serializer/tree/1.6.0) (2017-03-24) - -**Implemented enhancements:** - -- Add DateTimeImmutable support to DateHandler [\#543](https://github.com/schmittjoh/serializer/issues/543) - -**Fixed bugs:** +- Doctrine driver normalizeFieldType method does not handle nulls [\#1020](https://github.com/schmittjoh/serializer/pull/1020) ([goetas](https://github.com/goetas)) +- fixed a typo [\#1014](https://github.com/schmittjoh/serializer/pull/1014) ([themasch](https://github.com/themasch)) -- Virtual property having type overriden by doctrine metadata [\#276](https://github.com/schmittjoh/serializer/issues/276) +## [2.0.1](https://github.com/schmittjoh/serializer/tree/2.0.1) (2018-11-29) -**Closed issues:** +## [2.0.0](https://github.com/schmittjoh/serializer/tree/2.0.0) (2018-11-09) -- Serialize a subclass [\#735](https://github.com/schmittjoh/serializer/issues/735) -- How to handle Doctrine not found entity ? [\#731](https://github.com/schmittjoh/serializer/issues/731) -- Regression with 1.5.0 =\> Undefined offset 15 [\#715](https://github.com/schmittjoh/serializer/issues/715) -- detect serialisation without groups set [\#546](https://github.com/schmittjoh/serializer/issues/546) -- Introducing the NormalizerInterface [\#537](https://github.com/schmittjoh/serializer/issues/537) -- How to set JSON serialization options? [\#535](https://github.com/schmittjoh/serializer/issues/535) -- @MaxDepth doesn't seem to be working [\#522](https://github.com/schmittjoh/serializer/issues/522) -- max\_depth in YML config is ignored [\#498](https://github.com/schmittjoh/serializer/issues/498) -- Dynamic property type annotation [\#436](https://github.com/schmittjoh/serializer/issues/436) -- How to deserialize JSON if property might have a list of subobjects? [\#355](https://github.com/schmittjoh/serializer/issues/355) -- Object to array normalization [\#354](https://github.com/schmittjoh/serializer/issues/354) -- Serialize Doctrine object without references [\#353](https://github.com/schmittjoh/serializer/issues/353) -- Post\_serialize doesn't serialize relation! [\#236](https://github.com/schmittjoh/serializer/issues/236) -- parsing string to date [\#217](https://github.com/schmittjoh/serializer/issues/217) -- Discriminator is not exposed when using a group exclusion strategy [\#157](https://github.com/schmittjoh/serializer/issues/157) +## [2.0.0-RC1](https://github.com/schmittjoh/serializer/tree/2.0.0-RC1) (2018-10-17) -## [1.6.0-RC1](https://github.com/schmittjoh/serializer/tree/1.6.0-RC1) (2017-03-14) +## [2.0.0-beta1](https://github.com/schmittjoh/serializer/tree/2.0.0-beta1) (2018-09-12) -**Implemented enhancements:** +## [1.13.0](https://github.com/schmittjoh/serializer/tree/1.13.0) (2018-07-25) -- Add symfony expression in exclusions/expositions [\#406](https://github.com/schmittjoh/serializer/issues/406) -- check that cache directory was not created before throwing exception [\#729](https://github.com/schmittjoh/serializer/pull/729) ([mente](https://github.com/mente)) -- \#720 - Adding support for DateInterval deserialization [\#721](https://github.com/schmittjoh/serializer/pull/721) ([c0ntax](https://github.com/c0ntax)) -- Expression language based virtual properties [\#708](https://github.com/schmittjoh/serializer/pull/708) ([goetas](https://github.com/goetas)) -- Added clearing previous libxml errors [\#688](https://github.com/schmittjoh/serializer/pull/688) ([zerkms](https://github.com/zerkms)) -- Xml namespaces improvements [\#644](https://github.com/schmittjoh/serializer/pull/644) ([goetas](https://github.com/goetas)) +## [1.12.1](https://github.com/schmittjoh/serializer/tree/1.12.1) (2018-06-01) -**Fixed bugs:** +## [1.12.0](https://github.com/schmittjoh/serializer/tree/1.12.0) (2018-05-25) -- Serialize correctly empty intervals according to ISO-8601 [\#722](https://github.com/schmittjoh/serializer/pull/722) ([goetas](https://github.com/goetas)) +## [1.11.0](https://github.com/schmittjoh/serializer/tree/1.11.0) (2018-02-04) -**Closed issues:** +## [1.10.0](https://github.com/schmittjoh/serializer/tree/1.10.0) (2017-11-30) -- Is it possible to achieve something like - shouldSerializeEmpty [\#725](https://github.com/schmittjoh/serializer/issues/725) -- How to handle DateTime serialization with fromArray method ? [\#723](https://github.com/schmittjoh/serializer/issues/723) -- DateInterval supported for serialization but not deserialization [\#720](https://github.com/schmittjoh/serializer/issues/720) -- Deserialization of collection when wraped by aditional xml tags [\#719](https://github.com/schmittjoh/serializer/issues/719) -- SerializedName based on a property value [\#716](https://github.com/schmittjoh/serializer/issues/716) -- Blank XML breaks XmlDeserializationVisitor error handling [\#701](https://github.com/schmittjoh/serializer/issues/701) -- Problem with FOSUserBundle ROLE serialization [\#690](https://github.com/schmittjoh/serializer/issues/690) -- Doctrine cache dependency when using setCacheDir [\#676](https://github.com/schmittjoh/serializer/issues/676) -- OneToOne entities are not deserialized if passing a nested one-to-one property [\#652](https://github.com/schmittjoh/serializer/issues/652) -- \[RFC\] Serialization refacotring [\#609](https://github.com/schmittjoh/serializer/issues/609) -- Object handler callback returns array, but serialized object = null [\#594](https://github.com/schmittjoh/serializer/issues/594) -- Cannot add @Discriminator field into specific @Group [\#557](https://github.com/schmittjoh/serializer/issues/557) -- Object check on SerializationContext::isVisiting\(\) [\#502](https://github.com/schmittjoh/serializer/issues/502) -- Define cdata and namespace for @XmlList elements [\#480](https://github.com/schmittjoh/serializer/issues/480) -- Serializer working with parent class [\#376](https://github.com/schmittjoh/serializer/issues/376) -- Add support for array format [\#374](https://github.com/schmittjoh/serializer/issues/374) -- Obtain VirtualProperty value using a service [\#359](https://github.com/schmittjoh/serializer/issues/359) -- make deserialisation of non existing id's configurable [\#333](https://github.com/schmittjoh/serializer/issues/333) -- HHVM compatibility issue with undefined property JMS\Serializer\Metadata\ClassMetadata::$inline [\#312](https://github.com/schmittjoh/serializer/issues/312) -- resources serialization [\#275](https://github.com/schmittjoh/serializer/issues/275) -- I'm receiving "Class ArrayCollection does not exist" when serializing \(temporarily solved with a workaround\) [\#274](https://github.com/schmittjoh/serializer/issues/274) -- Can't use handlers on strings \(and other simple types\) [\#194](https://github.com/schmittjoh/serializer/issues/194) -- composer.json update for doctrine [\#178](https://github.com/schmittjoh/serializer/issues/178) -- Use expression for virtual properties [\#171](https://github.com/schmittjoh/serializer/issues/171) -- Handle classes that implement collections \(e.g. ArrayObject\) and properties [\#137](https://github.com/schmittjoh/serializer/issues/137) -- Check CDATA is needed [\#136](https://github.com/schmittjoh/serializer/issues/136) -- property path support [\#22](https://github.com/schmittjoh/serializer/issues/22) +## [1.9.2](https://github.com/schmittjoh/serializer/tree/1.9.2) (2017-11-22) -**Merged pull requests:** +## [1.9.1](https://github.com/schmittjoh/serializer/tree/1.9.1) (2017-10-27) -- Include reference to cache [\#727](https://github.com/schmittjoh/serializer/pull/727) ([hyperized](https://github.com/hyperized)) -- A possible fix for the \#688 [\#689](https://github.com/schmittjoh/serializer/pull/689) ([zerkms](https://github.com/zerkms)) +## [1.9.0](https://github.com/schmittjoh/serializer/tree/1.9.0) (2017-09-28) -## [1.5.0](https://github.com/schmittjoh/serializer/tree/1.5.0) (2017-02-14) +## [1.8.1](https://github.com/schmittjoh/serializer/tree/1.8.1) (2017-07-13) -**Fixed bugs:** +## [1.8.0](https://github.com/schmittjoh/serializer/tree/1.8.0) (2017-07-12) -- Deserializing XMLList with Namespaces not \(always\) working as intended [\#697](https://github.com/schmittjoh/serializer/pull/697) ([goetas](https://github.com/goetas)) +## [1.7.1](https://github.com/schmittjoh/serializer/tree/1.7.1) (2017-05-15) -**Closed issues:** +## [1.7.0](https://github.com/schmittjoh/serializer/tree/1.7.0) (2017-05-10) -- Serialized DateTime instances are not valid ISO-8601 [\#713](https://github.com/schmittjoh/serializer/issues/713) -- Impossible to use discriminator field. Why we need StaticPropertyMetadata ? [\#705](https://github.com/schmittjoh/serializer/issues/705) -- Deserializing XMLList with Namespaces not \(always\) working as intended [\#695](https://github.com/schmittjoh/serializer/issues/695) +## [1.7.0-RC2](https://github.com/schmittjoh/serializer/tree/1.7.0-RC2) (2017-05-05) -## [1.5.0-RC1](https://github.com/schmittjoh/serializer/tree/1.5.0-RC1) (2017-01-19) +## [1.7.0-RC1](https://github.com/schmittjoh/serializer/tree/1.7.0-RC1) (2017-04-25) -**Implemented enhancements:** +## [1.6.2](https://github.com/schmittjoh/serializer/tree/1.6.2) (2017-04-17) -- added support for xml-attributes as discriminators [\#692](https://github.com/schmittjoh/serializer/pull/692) ([twtinteractive](https://github.com/twtinteractive)) -- Prevent doctrine proxy loading for virtual types [\#684](https://github.com/schmittjoh/serializer/pull/684) ([goetas](https://github.com/goetas)) -- Implemented dynamic exclusion using symfony expression language [\#673](https://github.com/schmittjoh/serializer/pull/673) ([goetas](https://github.com/goetas)) -- Issue543 - Adding DateTimeImmutable support [\#635](https://github.com/schmittjoh/serializer/pull/635) ([toby-griffiths](https://github.com/toby-griffiths)) +## [1.6.1](https://github.com/schmittjoh/serializer/tree/1.6.1) (2017-04-12) -**Closed issues:** +## [1.6.0](https://github.com/schmittjoh/serializer/tree/1.6.0) (2017-03-24) -- Groups logic [\#693](https://github.com/schmittjoh/serializer/issues/693) -- BC from 1.1.\* to ^1.2 [\#643](https://github.com/schmittjoh/serializer/issues/643) -- DoctrineProxySubscriber forces loading of the proxy even if custom handler exist [\#575](https://github.com/schmittjoh/serializer/issues/575) -- ConditionalExpose/Exclude annotation [\#540](https://github.com/schmittjoh/serializer/issues/540) -- Deprecated usage of ValidatorInterface [\#438](https://github.com/schmittjoh/serializer/issues/438) -- Missing addData in XmlSerializerVisitor makes it impossible to add data in serializer.post\_serialize event [\#235](https://github.com/schmittjoh/serializer/issues/235) -- Support JSON PATCH for updating object graph [\#231](https://github.com/schmittjoh/serializer/issues/231) -- Dynamic expose, aka 'fields' query param [\#195](https://github.com/schmittjoh/serializer/issues/195) +## [1.6.0-RC1](https://github.com/schmittjoh/serializer/tree/1.6.0-RC1) (2017-03-14) -**Merged pull requests:** +## [1.5.0](https://github.com/schmittjoh/serializer/tree/1.5.0) (2017-02-14) -- Added doc reference for disabling discriminator [\#699](https://github.com/schmittjoh/serializer/pull/699) ([dragosprotung](https://github.com/dragosprotung)) -- Use GroupsExclusionStrategy::DEFAULT\_GROUP instead default group. [\#694](https://github.com/schmittjoh/serializer/pull/694) ([Aliance](https://github.com/Aliance)) -- Improved Symfony 3.x compatibility [\#682](https://github.com/schmittjoh/serializer/pull/682) ([goetas](https://github.com/goetas)) -- Discriminator Groups [\#579](https://github.com/schmittjoh/serializer/pull/579) ([maennchen](https://github.com/maennchen)) -- Add extra test for handling child elements [\#569](https://github.com/schmittjoh/serializer/pull/569) ([tarjei](https://github.com/tarjei)) +## [1.5.0-RC1](https://github.com/schmittjoh/serializer/tree/1.5.0-RC1) (2017-01-19) ## [1.4.2](https://github.com/schmittjoh/serializer/tree/1.4.2) (2016-11-13) -**Fixed bugs:** - -- Warning: JMS\Serializer\XmlDeserializationVisitor::visitArray\(\): Node no longer exists [\#674](https://github.com/schmittjoh/serializer/issues/674) -- Fixed xml arrays with namespaced entry triggers error [\#675](https://github.com/schmittjoh/serializer/pull/675) ([goetas](https://github.com/goetas)) - -**Closed issues:** - -- Max depth produces array of nulls [\#671](https://github.com/schmittjoh/serializer/issues/671) - ## [1.4.1](https://github.com/schmittjoh/serializer/tree/1.4.1) (2016-11-02) -**Fixed bugs:** - -- Groups context might be not initialized [\#669](https://github.com/schmittjoh/serializer/pull/669) ([goetas](https://github.com/goetas)) - -**Closed issues:** - -- Warning: Invalid argument supplied for foreach\(\) on getCurrentPath method [\#668](https://github.com/schmittjoh/serializer/issues/668) - ## [1.4.0](https://github.com/schmittjoh/serializer/tree/1.4.0) (2016-10-31) -**Implemented enhancements:** - -- Document the implied 'Default' property group when no group is specified [\#661](https://github.com/schmittjoh/serializer/pull/661) ([akoebbe](https://github.com/akoebbe)) -- Allow discriminator map in the middle of the hierarchy when deserializing [\#659](https://github.com/schmittjoh/serializer/pull/659) ([goetas](https://github.com/goetas)) -- Handle both int and integer [\#657](https://github.com/schmittjoh/serializer/pull/657) ([Aliance](https://github.com/Aliance)) -- Can now override groups on specific paths of the graph [\#170](https://github.com/schmittjoh/serializer/pull/170) ([adrienbrault](https://github.com/adrienbrault)) - -**Fixed bugs:** - -- Deserialization fails when discriminator base class extends another class [\#182](https://github.com/schmittjoh/serializer/issues/182) -- Xml setters ignored when deserializing [\#665](https://github.com/schmittjoh/serializer/pull/665) ([goetas](https://github.com/goetas)) - -**Closed issues:** - -- Move `FormErrorHandler` to the bundle [\#664](https://github.com/schmittjoh/serializer/issues/664) -- Not compatible with Symfony 3's Controller::json\(\) [\#663](https://github.com/schmittjoh/serializer/issues/663) -- Class name not reflecting in serialized json [\#662](https://github.com/schmittjoh/serializer/issues/662) -- YML virtual\_properties no group exlcusion [\#656](https://github.com/schmittjoh/serializer/issues/656) -- \[RFC\] Introduce normalizer\denormalizer interface [\#646](https://github.com/schmittjoh/serializer/issues/646) -- Plain arrays are serialized \(normalized\) as "objects", ignoring serializeNull [\#641](https://github.com/schmittjoh/serializer/issues/641) -- serializer doesn't serialize traits [\#638](https://github.com/schmittjoh/serializer/issues/638) -- Add metadata informations [\#637](https://github.com/schmittjoh/serializer/issues/637) -- Unexpected results when serializing arrays containing null value elements [\#593](https://github.com/schmittjoh/serializer/issues/593) -- Allow to set default serialization context when building serializer [\#528](https://github.com/schmittjoh/serializer/issues/528) -- Enable Sourcegraph [\#455](https://github.com/schmittjoh/serializer/issues/455) -- Use different accessor for each group [\#420](https://github.com/schmittjoh/serializer/issues/420) -- GenericSerializationVisitor and shouldSerializeNull [\#360](https://github.com/schmittjoh/serializer/issues/360) -- Specify group along with MaxDepth [\#150](https://github.com/schmittjoh/serializer/issues/150) -- Allow Post Serialize Event to overwrite existing data [\#129](https://github.com/schmittjoh/serializer/issues/129) -- Warning: array\_key\_exists\(\) expects parameter 2 to be array, string given [\#70](https://github.com/schmittjoh/serializer/issues/70) - -**Merged pull requests:** - -- Nullable array inconsistency [\#660](https://github.com/schmittjoh/serializer/pull/660) ([goetas](https://github.com/goetas)) -- Fixed PHP 7.0.11 BC break \(or bugfix\) [\#658](https://github.com/schmittjoh/serializer/pull/658) ([goetas](https://github.com/goetas)) -- Renamed replaceData to setData [\#653](https://github.com/schmittjoh/serializer/pull/653) ([goetas](https://github.com/goetas)) -- add required sqlite extension for developing [\#649](https://github.com/schmittjoh/serializer/pull/649) ([scasei](https://github.com/scasei)) -- Run serialization benchmarks in the build process [\#647](https://github.com/schmittjoh/serializer/pull/647) ([goetas](https://github.com/goetas)) -- Alcalyn feature default serializer context [\#645](https://github.com/schmittjoh/serializer/pull/645) ([goetas](https://github.com/goetas)) -- Add format output option [\#640](https://github.com/schmittjoh/serializer/pull/640) ([AyrtonRicardo](https://github.com/AyrtonRicardo)) -- Remove deprecated FileCacheReader for doctrine annotations [\#634](https://github.com/schmittjoh/serializer/pull/634) ([goetas](https://github.com/goetas)) -- Added tests to ensure SerializeNull policy [\#633](https://github.com/schmittjoh/serializer/pull/633) ([goetas](https://github.com/goetas)) -- Revert "Default `$serializeNull` to false" [\#630](https://github.com/schmittjoh/serializer/pull/630) ([goetas](https://github.com/goetas)) -- Introducing NormalizerInterface [\#592](https://github.com/schmittjoh/serializer/pull/592) ([alcalyn](https://github.com/alcalyn)) -- Fix inheritance of discriminators on Doctrine entities [\#382](https://github.com/schmittjoh/serializer/pull/382) ([xoob](https://github.com/xoob)) -- Allow Post Serialize Event to overwrite existing data [\#273](https://github.com/schmittjoh/serializer/pull/273) ([jockri](https://github.com/jockri)) - ## [1.3.1](https://github.com/schmittjoh/serializer/tree/1.3.1) (2016-08-23) -**Closed issues:** - -- \[Idea\] Inline name [\#629](https://github.com/schmittjoh/serializer/issues/629) -- indexBy property doesn't work since 1.2.0 [\#618](https://github.com/schmittjoh/serializer/issues/618) -- Composer deps issue [\#494](https://github.com/schmittjoh/serializer/issues/494) -- PHP 7 compatability issue [\#478](https://github.com/schmittjoh/serializer/issues/478) -- Add new tag \(upgrade packagist\) [\#461](https://github.com/schmittjoh/serializer/issues/461) -- Custom Type Handler for String Values [\#384](https://github.com/schmittjoh/serializer/issues/384) -- serializer ignores properties added by traits [\#313](https://github.com/schmittjoh/serializer/issues/313) -- Skip an element during Xml deserialization process [\#229](https://github.com/schmittjoh/serializer/issues/229) -- Using serializer for JSON serialising [\#223](https://github.com/schmittjoh/serializer/issues/223) -- No way to serialize binary data with a custom type [\#202](https://github.com/schmittjoh/serializer/issues/202) -- Automatic mapping of properties [\#200](https://github.com/schmittjoh/serializer/issues/200) -- Maybe the serializer should also allow the legal literals {1, 0} for booleans [\#198](https://github.com/schmittjoh/serializer/issues/198) -- Customize how Booleans are serialized [\#180](https://github.com/schmittjoh/serializer/issues/180) -- Problem with deserialize related entity [\#123](https://github.com/schmittjoh/serializer/issues/123) -- serialized\_name does not work in yaml [\#118](https://github.com/schmittjoh/serializer/issues/118) - ## [1.3.0](https://github.com/schmittjoh/serializer/tree/1.3.0) (2016-08-17) -**Fixed bugs:** - -- Fix warning array\_key\_exists in deserialization. [\#398](https://github.com/schmittjoh/serializer/pull/398) ([leonnleite](https://github.com/leonnleite)) - -**Closed issues:** - -- problems with xml namespaces after update [\#621](https://github.com/schmittjoh/serializer/issues/621) -- Trying to decorate a member to ArrayCollection but gets an error when deserilizing because composer didn't download the class from doctrine. [\#596](https://github.com/schmittjoh/serializer/issues/596) -- Missing doctrine/common requirement ? [\#517](https://github.com/schmittjoh/serializer/issues/517) -- PHP Fatal error: Using $this when not in object context in JMS/Serializer/Serializer.php on line 99 [\#441](https://github.com/schmittjoh/serializer/issues/441) -- custom collection handler [\#415](https://github.com/schmittjoh/serializer/issues/415) -- Exclude annotation not preventing attempt to find public methods when using AccessType [\#367](https://github.com/schmittjoh/serializer/issues/367) -- serializer.pre\_serialize event only thrown on objects/classes [\#337](https://github.com/schmittjoh/serializer/issues/337) -- Installing through composer gets "Segmentation fault" [\#308](https://github.com/schmittjoh/serializer/issues/308) -- Erroneous data format for unserializing... [\#283](https://github.com/schmittjoh/serializer/issues/283) -- DoctrineObjectConstructor should skip empty identifier field [\#193](https://github.com/schmittjoh/serializer/issues/193) - -**Merged pull requests:** - -- Added public `hasData` function to check if a data key already have been added. [\#625](https://github.com/schmittjoh/serializer/pull/625) ([goetas](https://github.com/goetas)) -- $context is not used [\#622](https://github.com/schmittjoh/serializer/pull/622) ([olvlvl](https://github.com/olvlvl)) -- Fix Doctrine PHPCR ODM 2.0 compatibility [\#605](https://github.com/schmittjoh/serializer/pull/605) ([wouterj](https://github.com/wouterj)) -- Fixed type-hinting [\#586](https://github.com/schmittjoh/serializer/pull/586) ([jgendera](https://github.com/jgendera)) -- Fix multiple handler callbacks in YamlDriver [\#515](https://github.com/schmittjoh/serializer/pull/515) ([mpajunen](https://github.com/mpajunen)) -- Fixed minor typos [\#364](https://github.com/schmittjoh/serializer/pull/364) ([sdaoudi](https://github.com/sdaoudi)) -- Default `$serializeNull` to false [\#317](https://github.com/schmittjoh/serializer/pull/317) ([steveYeah](https://github.com/steveYeah)) -- Missing attribute 'xml-value' in XML Reference [\#269](https://github.com/schmittjoh/serializer/pull/269) ([holtkamp](https://github.com/holtkamp)) -- Removed unnecessary use statement [\#262](https://github.com/schmittjoh/serializer/pull/262) ([dunglas](https://github.com/dunglas)) - ## [1.2.0](https://github.com/schmittjoh/serializer/tree/1.2.0) (2016-08-03) -**Fixed bugs:** - -- Fix xml-attribute-map for the xml driver [\#595](https://github.com/schmittjoh/serializer/pull/595) ([romantomchak](https://github.com/romantomchak)) -- \#367 Exclude annotation not preventing attempt to find public methods when using AccessType [\#397](https://github.com/schmittjoh/serializer/pull/397) ([Strate](https://github.com/Strate)) - -**Closed issues:** - -- XML serialisation performance vs. SimpleXML? [\#606](https://github.com/schmittjoh/serializer/issues/606) -- Undefined Offset 21 - PropertyMetadata \(master\) [\#581](https://github.com/schmittjoh/serializer/issues/581) -- Invalid null serialization in arrays [\#571](https://github.com/schmittjoh/serializer/issues/571) -- List Polymorphic with XML Deserialization [\#568](https://github.com/schmittjoh/serializer/issues/568) -- Serialize null values as empty string [\#566](https://github.com/schmittjoh/serializer/issues/566) -- Type mismatch should throw an exception instead of coercing when deserializing JSON [\#561](https://github.com/schmittjoh/serializer/issues/561) -- Serialize to array [\#518](https://github.com/schmittjoh/serializer/issues/518) -- AnnotationDriver Exception on Missing Setter/Getter even on @Exclude'd Properties [\#516](https://github.com/schmittjoh/serializer/issues/516) -- Arrays are serialized as objects like {"0":... } when data contains empty objects [\#488](https://github.com/schmittjoh/serializer/issues/488) -- Tag new release [\#465](https://github.com/schmittjoh/serializer/issues/465) -- Forcing no scientific notation for larg number, type double [\#405](https://github.com/schmittjoh/serializer/issues/405) -- PHP \< 5.3.9 BC break [\#383](https://github.com/schmittjoh/serializer/issues/383) -- Ignoring a tag when deserializing [\#352](https://github.com/schmittjoh/serializer/issues/352) - -**Merged pull requests:** - -- Allow to not skip empty not inline array root node [\#611](https://github.com/schmittjoh/serializer/pull/611) ([goetas](https://github.com/goetas)) -- Allow to use custom serializer with primitive type [\#610](https://github.com/schmittjoh/serializer/pull/610) ([goetas](https://github.com/goetas)) -- Composer is not able to resolve a dependency [\#608](https://github.com/schmittjoh/serializer/pull/608) ([goetas](https://github.com/goetas)) -- Test on Travis always high and low deps [\#584](https://github.com/schmittjoh/serializer/pull/584) ([goetas](https://github.com/goetas)) -- Update Symfony validator and allow PHPUnit 7 [\#583](https://github.com/schmittjoh/serializer/pull/583) ([goetas](https://github.com/goetas)) -- Fix serialize bug [\#582](https://github.com/schmittjoh/serializer/pull/582) ([goetas](https://github.com/goetas)) -- HHVM compatibility [\#580](https://github.com/schmittjoh/serializer/pull/580) ([goetas](https://github.com/goetas)) -- Fixed test suite on master [\#578](https://github.com/schmittjoh/serializer/pull/578) ([goetas](https://github.com/goetas)) -- Fix for a broken test: a missing \(incorrectly positioned\) argument [\#577](https://github.com/schmittjoh/serializer/pull/577) ([zerkms](https://github.com/zerkms)) -- Fix bug \#343 return integer when the column is datetime [\#562](https://github.com/schmittjoh/serializer/pull/562) ([Bukashk0zzz](https://github.com/Bukashk0zzz)) -- \[doc\] fix AccessorOrder documentation [\#553](https://github.com/schmittjoh/serializer/pull/553) ([aledeg](https://github.com/aledeg)) -- Generic way to solve setValue on a property which respects its setter [\#550](https://github.com/schmittjoh/serializer/pull/550) ([maennchen](https://github.com/maennchen)) -- Added travis-ci label [\#399](https://github.com/schmittjoh/serializer/pull/399) ([spolischook](https://github.com/spolischook)) -- Generate namespaced element on XmlList entries [\#301](https://github.com/schmittjoh/serializer/pull/301) ([goetas](https://github.com/goetas)) - ## [1.1.0](https://github.com/schmittjoh/serializer/tree/1.1.0) (2015-10-27) -**Closed issues:** - -- Possible to set xsi:schemalocation? [\#505](https://github.com/schmittjoh/serializer/issues/505) -- Travis needs a renewed token to be able to set the status [\#495](https://github.com/schmittjoh/serializer/issues/495) -- Serialize a many-to-many relation [\#474](https://github.com/schmittjoh/serializer/issues/474) -- The document type "..." is not allowed [\#427](https://github.com/schmittjoh/serializer/issues/427) -- Yml serializer don't serialize empty arrays [\#183](https://github.com/schmittjoh/serializer/issues/183) - -**Merged pull requests:** - -- Manage empty array for serializer [\#510](https://github.com/schmittjoh/serializer/pull/510) ([Soullivaneuh](https://github.com/Soullivaneuh)) -- Fix the method name for the serialization context factory [\#490](https://github.com/schmittjoh/serializer/pull/490) ([stof](https://github.com/stof)) -- Switch the Twig integration to use non-deprecated APIs [\#482](https://github.com/schmittjoh/serializer/pull/482) ([stof](https://github.com/stof)) -- Add PHP 7 on Travis [\#477](https://github.com/schmittjoh/serializer/pull/477) ([Soullivaneuh](https://github.com/Soullivaneuh)) -- Change Proxy class used to Doctrine\Common\Persistence\Proxy [\#351](https://github.com/schmittjoh/serializer/pull/351) ([bburnichon](https://github.com/bburnichon)) -- Added PHP 5.6 [\#297](https://github.com/schmittjoh/serializer/pull/297) ([Nyholm](https://github.com/Nyholm)) - ## [1.0.0](https://github.com/schmittjoh/serializer/tree/1.0.0) (2015-06-16) -**Closed issues:** - -- Unrecognized 4 parts namespace [\#449](https://github.com/schmittjoh/serializer/issues/449) -- Groups is ignored [\#440](https://github.com/schmittjoh/serializer/issues/440) -- Property FelDev\CoreBundle\Entity\Persona::$apellido does not exist [\#432](https://github.com/schmittjoh/serializer/issues/432) -- Erroneous data format for unserializing [\#430](https://github.com/schmittjoh/serializer/issues/430) -- Deserialize JSON into existing Doctrine entities and empty strings are ignored [\#417](https://github.com/schmittjoh/serializer/issues/417) -- Failing to deserealize JSON string [\#402](https://github.com/schmittjoh/serializer/issues/402) -- Empty results serializing virtual\_properties [\#400](https://github.com/schmittjoh/serializer/issues/400) -- API stable 1.0.0 release in sight? [\#395](https://github.com/schmittjoh/serializer/issues/395) -- Is this project maintained still? [\#361](https://github.com/schmittjoh/serializer/issues/361) -- PreSerialize [\#339](https://github.com/schmittjoh/serializer/issues/339) -- Change default `access\_type` globally [\#336](https://github.com/schmittjoh/serializer/issues/336) -- Deserialization of XmlList does not support namespaces [\#332](https://github.com/schmittjoh/serializer/issues/332) -- Recursion groups, serializing properties in entities [\#329](https://github.com/schmittjoh/serializer/issues/329) -- The testsuite is broken [\#326](https://github.com/schmittjoh/serializer/issues/326) -- Namespaces and serialize/deserialize process [\#303](https://github.com/schmittjoh/serializer/issues/303) -- Exclusion of parent properties failing [\#282](https://github.com/schmittjoh/serializer/issues/282) -- How to deserialize correctly an array of arbitrary values ? [\#280](https://github.com/schmittjoh/serializer/issues/280) -- Try to identify getter/setter from an excluded property [\#278](https://github.com/schmittjoh/serializer/issues/278) -- Bug Entity constructor not called [\#270](https://github.com/schmittjoh/serializer/issues/270) -- Make it possible to escape special characters on serialization [\#265](https://github.com/schmittjoh/serializer/issues/265) -- doctrine annotations without namespace [\#264](https://github.com/schmittjoh/serializer/issues/264) -- php-collection constraint [\#257](https://github.com/schmittjoh/serializer/issues/257) -- \[Metadata\] PHP warning only when unittesting [\#255](https://github.com/schmittjoh/serializer/issues/255) -- Discriminator [\#220](https://github.com/schmittjoh/serializer/issues/220) - -**Merged pull requests:** - -- fix json output \(from \[\] to {} if empty\) of form error [\#462](https://github.com/schmittjoh/serializer/pull/462) ([jhkchan](https://github.com/jhkchan)) -- Add toArray and fromArray methods to the serializer [\#435](https://github.com/schmittjoh/serializer/pull/435) ([tystr](https://github.com/tystr)) -- Erroneous data format for unserializing \#430 [\#431](https://github.com/schmittjoh/serializer/pull/431) ([tmilos](https://github.com/tmilos)) -- Scrutinizer Auto-Fixes [\#381](https://github.com/schmittjoh/serializer/pull/381) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) -- Fixing tests for bugfixed PHP versions [\#375](https://github.com/schmittjoh/serializer/pull/375) ([urakozz](https://github.com/urakozz)) -- Making test running against phpunit 4.\* [\#369](https://github.com/schmittjoh/serializer/pull/369) ([joelwurtz](https://github.com/joelwurtz)) -- Fixes a typo in the annotations.rst [\#363](https://github.com/schmittjoh/serializer/pull/363) ([Potherca](https://github.com/Potherca)) -- \[doc\] Default group informations [\#345](https://github.com/schmittjoh/serializer/pull/345) ([emilien-puget](https://github.com/emilien-puget)) -- bump branch alias to 0.17 as 0.16 is already released [\#305](https://github.com/schmittjoh/serializer/pull/305) ([lsmith77](https://github.com/lsmith77)) -- Unserialization of XML booleans [\#302](https://github.com/schmittjoh/serializer/pull/302) ([goetas](https://github.com/goetas)) -- Added xml\_root\_namespace on YAML reference [\#299](https://github.com/schmittjoh/serializer/pull/299) ([goetas](https://github.com/goetas)) -- Fixed yml mapping file name [\#256](https://github.com/schmittjoh/serializer/pull/256) ([spolischook](https://github.com/spolischook)) -- Serialization of nested polymorphic objects [\#238](https://github.com/schmittjoh/serializer/pull/238) ([DavidMikeSimon](https://github.com/DavidMikeSimon)) - ## [0.16.0](https://github.com/schmittjoh/serializer/tree/0.16.0) (2014-03-18) -**Closed issues:** - -- best way to add root to json? [\#250](https://github.com/schmittjoh/serializer/issues/250) -- Use Doctrine metadata [\#247](https://github.com/schmittjoh/serializer/issues/247) -- Integration Points - run-time exclusion checking [\#239](https://github.com/schmittjoh/serializer/issues/239) -- Using DoctrineTypeDriver to use Doctrine Anotations [\#232](https://github.com/schmittjoh/serializer/issues/232) -- Virtual property documentation xml & yaml [\#100](https://github.com/schmittjoh/serializer/issues/100) - -**Merged pull requests:** - -- Changed some constraint to allow latest versions [\#251](https://github.com/schmittjoh/serializer/pull/251) ([stof](https://github.com/stof)) -- XML root element namespace support [\#246](https://github.com/schmittjoh/serializer/pull/246) ([andreasferber](https://github.com/andreasferber)) -- Added test for leading backslash in front of class name to TypeParserTest [\#245](https://github.com/schmittjoh/serializer/pull/245) ([deralex](https://github.com/deralex)) -- Allow to fetch data from has\*\(\) with public\_method [\#243](https://github.com/schmittjoh/serializer/pull/243) ([jaymecd](https://github.com/jaymecd)) -- Improve yaml documentacion Fix \#100 [\#221](https://github.com/schmittjoh/serializer/pull/221) ([BraisGabin](https://github.com/BraisGabin)) - ## [0.15.0](https://github.com/schmittjoh/serializer/tree/0.15.0) (2014-02-10) -**Closed issues:** - -- Add trait support [\#228](https://github.com/schmittjoh/serializer/issues/228) -- "array" type: Not working for arrays of DateTime objects [\#199](https://github.com/schmittjoh/serializer/issues/199) -- Discriminator field filtered by exclusion strategy [\#189](https://github.com/schmittjoh/serializer/issues/189) -- DateTime within an array \(format get ignored\) [\#140](https://github.com/schmittjoh/serializer/issues/140) -- EntityNotFoundException using softDeletable [\#101](https://github.com/schmittjoh/serializer/issues/101) - -**Merged pull requests:** - -- Read only class [\#227](https://github.com/schmittjoh/serializer/pull/227) ([goetas](https://github.com/goetas)) -- @Alex88's Serialize only form child of type Form \#117 [\#224](https://github.com/schmittjoh/serializer/pull/224) ([minayaserrano](https://github.com/minayaserrano)) -- @XmlElement notation consistency [\#219](https://github.com/schmittjoh/serializer/pull/219) ([ajgarlag](https://github.com/ajgarlag)) -- add $this-\>maxDepth to serialize / unserialize [\#218](https://github.com/schmittjoh/serializer/pull/218) ([rothfahl](https://github.com/rothfahl)) -- xml reference updated with virtual-property example [\#215](https://github.com/schmittjoh/serializer/pull/215) ([ribeiropaulor](https://github.com/ribeiropaulor)) -- Add XmlNamespace annotation documentation [\#213](https://github.com/schmittjoh/serializer/pull/213) ([jeserkin](https://github.com/jeserkin)) -- Scrutinizer Auto-Fixes [\#210](https://github.com/schmittjoh/serializer/pull/210) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) -- Scrutinizer Auto-Fixes [\#206](https://github.com/schmittjoh/serializer/pull/206) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) -- Add xmlAttributeMap to serialized values [\#204](https://github.com/schmittjoh/serializer/pull/204) ([colinfrei](https://github.com/colinfrei)) -- fix issue \#199: "array" type ignoring DateTime format [\#201](https://github.com/schmittjoh/serializer/pull/201) ([lukey78](https://github.com/lukey78)) -- Potential fix for "recursion detected" issue [\#104](https://github.com/schmittjoh/serializer/pull/104) ([tyler-sommer](https://github.com/tyler-sommer)) -- Adds XML namespaces support [\#58](https://github.com/schmittjoh/serializer/pull/58) ([ajgarlag](https://github.com/ajgarlag)) - ## [0.14.0](https://github.com/schmittjoh/serializer/tree/0.14.0) (2013-12-04) -**Closed issues:** - -- @HandlerCallback not inherited [\#181](https://github.com/schmittjoh/serializer/issues/181) -- Conditional serialization [\#173](https://github.com/schmittjoh/serializer/issues/173) -- Deserialize XML partially [\#167](https://github.com/schmittjoh/serializer/issues/167) -- getter is not called when serializing Discriminator parent entity [\#156](https://github.com/schmittjoh/serializer/issues/156) -- Deserialize DateTime from js Date.toJSON format fail [\#145](https://github.com/schmittjoh/serializer/issues/145) -- Yaml driver for the parameter xml\_attribute\_map is broken [\#141](https://github.com/schmittjoh/serializer/issues/141) -- XmlKeyValueStore annotation does not seem to deserialize properly [\#139](https://github.com/schmittjoh/serializer/issues/139) -- Boolean conversion gone wrong [\#134](https://github.com/schmittjoh/serializer/issues/134) -- Serialize to/from array? [\#133](https://github.com/schmittjoh/serializer/issues/133) -- @XmlRoot annotation no longer working [\#131](https://github.com/schmittjoh/serializer/issues/131) -- Skip an element based on a condition in a XmlList [\#121](https://github.com/schmittjoh/serializer/issues/121) - -**Merged pull requests:** - -- No CData [\#187](https://github.com/schmittjoh/serializer/pull/187) ([mvrhov](https://github.com/mvrhov)) -- composer is preinstalled on travis [\#185](https://github.com/schmittjoh/serializer/pull/185) ([lsmith77](https://github.com/lsmith77)) -- \[WIP\] added support for PHPCR [\#184](https://github.com/schmittjoh/serializer/pull/184) ([lsmith77](https://github.com/lsmith77)) -- Metadata filename convention added to yml/xml references [\#172](https://github.com/schmittjoh/serializer/pull/172) ([rodrigodiez](https://github.com/rodrigodiez)) -- Fix inline bug with empty child [\#165](https://github.com/schmittjoh/serializer/pull/165) ([adrienbrault](https://github.com/adrienbrault)) -- Add virtual properties yaml example [\#163](https://github.com/schmittjoh/serializer/pull/163) ([adrienbrault](https://github.com/adrienbrault)) -- Allow deserialization to constructed objects [\#160](https://github.com/schmittjoh/serializer/pull/160) ([eugene-dounar](https://github.com/eugene-dounar)) -- Fix DoctrineDriverTest random failures [\#155](https://github.com/schmittjoh/serializer/pull/155) ([eugene-dounar](https://github.com/eugene-dounar)) -- Fix XML null DateTime deserialization [\#154](https://github.com/schmittjoh/serializer/pull/154) ([eugene-dounar](https://github.com/eugene-dounar)) -- Update doctrine/orm dev dependency [\#153](https://github.com/schmittjoh/serializer/pull/153) ([eugene-dounar](https://github.com/eugene-dounar)) -- composer install --dev fails [\#152](https://github.com/schmittjoh/serializer/pull/152) ([eugene-dounar](https://github.com/eugene-dounar)) -- Update annotations.rst [\#146](https://github.com/schmittjoh/serializer/pull/146) ([chrisjohnson00](https://github.com/chrisjohnson00)) -- Add Doctrine\ODM\PHPCR\ChildrenCollection to ArrayCollectionHandler [\#143](https://github.com/schmittjoh/serializer/pull/143) ([hacfi](https://github.com/hacfi)) -- xml\_attribute\_map fix for the yaml driver [\#142](https://github.com/schmittjoh/serializer/pull/142) ([mvanmeerbeck](https://github.com/mvanmeerbeck)) -- fix wrong quote in used in docs [\#130](https://github.com/schmittjoh/serializer/pull/130) ([jaapio](https://github.com/jaapio)) -- Support PropelCollection serialization [\#81](https://github.com/schmittjoh/serializer/pull/81) ([zebraf1](https://github.com/zebraf1)) - ## [0.13.0](https://github.com/schmittjoh/serializer/tree/0.13.0) (2013-07-29) -**Closed issues:** - -- Documentation on Exclusion Strategies has an error [\#122](https://github.com/schmittjoh/serializer/issues/122) -- How access to the current serializing group in a subscriber ? [\#99](https://github.com/schmittjoh/serializer/issues/99) -- DoctrineProxySubscriber not found [\#93](https://github.com/schmittjoh/serializer/issues/93) -- Namespaces at root level [\#86](https://github.com/schmittjoh/serializer/issues/86) -- Issues when requesting JSON or XML using Doctrine MongoDB ODM [\#85](https://github.com/schmittjoh/serializer/issues/85) -- addGlobalIgnoredName not working [\#78](https://github.com/schmittjoh/serializer/issues/78) -- serialize\_null configuration [\#77](https://github.com/schmittjoh/serializer/issues/77) -- Add json prefix to prevent script tag csrf attack [\#76](https://github.com/schmittjoh/serializer/issues/76) -- Add support for replacing serialization object inside events [\#74](https://github.com/schmittjoh/serializer/issues/74) -- Next stable version? [\#64](https://github.com/schmittjoh/serializer/issues/64) -- Deserialize with object refs [\#62](https://github.com/schmittjoh/serializer/issues/62) - -**Merged pull requests:** - -- Document the handler $context argument [\#116](https://github.com/schmittjoh/serializer/pull/116) ([adrienbrault](https://github.com/adrienbrault)) -- Document the SubscribingHandlerInterface a bit [\#115](https://github.com/schmittjoh/serializer/pull/115) ([adrienbrault](https://github.com/adrienbrault)) -- Add getter for the xml serialization visitor defaultRootName property [\#114](https://github.com/schmittjoh/serializer/pull/114) ([adrienbrault](https://github.com/adrienbrault)) -- Add Serializer::getMetadataFactory [\#113](https://github.com/schmittjoh/serializer/pull/113) ([adrienbrault](https://github.com/adrienbrault)) -- Accessor order [\#108](https://github.com/schmittjoh/serializer/pull/108) ([jaapio](https://github.com/jaapio)) -- Added xmlns:xsi namespace and fixed tests [\#107](https://github.com/schmittjoh/serializer/pull/107) ([josser](https://github.com/josser)) -- \[Doc\] Fixed typo in event\_system [\#106](https://github.com/schmittjoh/serializer/pull/106) ([lyrixx](https://github.com/lyrixx)) -- Fix discriminator map search in ClassMetadata [\#97](https://github.com/schmittjoh/serializer/pull/97) ([xanido](https://github.com/xanido)) -- Use the AnnotationReader interface in the SerializerBuilder, instead of the implemented AnnotationReader itself [\#82](https://github.com/schmittjoh/serializer/pull/82) ([HarmenM](https://github.com/HarmenM)) -- Remove useless YamlSerializationVisitor::prepare method [\#75](https://github.com/schmittjoh/serializer/pull/75) ([adrienbrault](https://github.com/adrienbrault)) -- Add the PRE\_DESERIALIZE event to the Events class [\#73](https://github.com/schmittjoh/serializer/pull/73) ([adrienbrault](https://github.com/adrienbrault)) -- Improve serialization example [\#71](https://github.com/schmittjoh/serializer/pull/71) ([tvlooy](https://github.com/tvlooy)) -- Max depth strategy [\#4](https://github.com/schmittjoh/serializer/pull/4) ([adrienbrault](https://github.com/adrienbrault)) - ## [0.12.0](https://github.com/schmittjoh/serializer/tree/0.12.0) (2013-03-28) -**Closed issues:** - -- Serialization profile/definition builder [\#68](https://github.com/schmittjoh/serializer/issues/68) -- I want to configure the default exclution policy [\#65](https://github.com/schmittjoh/serializer/issues/65) -- Mulit type property mapping [\#56](https://github.com/schmittjoh/serializer/issues/56) -- AccessType\("public\_method"\): Setters ignored when deserializing to non-standard XML properties [\#53](https://github.com/schmittjoh/serializer/issues/53) -- Adding @Accessor with custom getter causes LogicException if Doctrine ManyToOneEntity [\#52](https://github.com/schmittjoh/serializer/issues/52) -- Handler callback's does not get passed context [\#49](https://github.com/schmittjoh/serializer/issues/49) -- PostSerialize callback causes data loss [\#46](https://github.com/schmittjoh/serializer/issues/46) -- Empty Objects get serialized as "array\(\)" [\#43](https://github.com/schmittjoh/serializer/issues/43) -- Exclusion Policies aren't properly applied when "serializeNull" is "true" [\#42](https://github.com/schmittjoh/serializer/issues/42) -- Accessor annotation ignored [\#40](https://github.com/schmittjoh/serializer/issues/40) -- Support for multiple exclusion strategies [\#39](https://github.com/schmittjoh/serializer/issues/39) -- srholt123@yahoo.com [\#35](https://github.com/schmittjoh/serializer/issues/35) -- Could you tag a stable version? [\#34](https://github.com/schmittjoh/serializer/issues/34) -- Default conversion of camelCase to underscores is counterintuitive [\#33](https://github.com/schmittjoh/serializer/issues/33) -- Define the xml root when deserializing [\#18](https://github.com/schmittjoh/serializer/issues/18) - -**Merged pull requests:** - -- \[Annotation\] Added the ability to set the type when using @VirtualProperty [\#69](https://github.com/schmittjoh/serializer/pull/69) ([pylebecq](https://github.com/pylebecq)) -- Added documentation for the @VirtualProperty annotation [\#67](https://github.com/schmittjoh/serializer/pull/67) ([pylebecq](https://github.com/pylebecq)) -- Metadata stack tests [\#57](https://github.com/schmittjoh/serializer/pull/57) ([adrienbrault](https://github.com/adrienbrault)) -- Adding context to twig extension [\#55](https://github.com/schmittjoh/serializer/pull/55) ([smurfy](https://github.com/smurfy)) -- Allow deserialization of polymorphic classes by class without specifying the type [\#48](https://github.com/schmittjoh/serializer/pull/48) ([gordalina](https://github.com/gordalina)) -- Moves all state to dedicated context class [\#47](https://github.com/schmittjoh/serializer/pull/47) ([schmittjoh](https://github.com/schmittjoh)) -- Add PropertyNamingStrategy [\#37](https://github.com/schmittjoh/serializer/pull/37) ([passkey1510](https://github.com/passkey1510)) -- The NavigatorContext now holds a metadata stack [\#28](https://github.com/schmittjoh/serializer/pull/28) ([adrienbrault](https://github.com/adrienbrault)) - ## [0.11.0](https://github.com/schmittjoh/serializer/tree/0.11.0) (2013-01-29) -**Closed issues:** - -- Hooking into metadata directly... [\#17](https://github.com/schmittjoh/serializer/issues/17) -- Serializing null values [\#14](https://github.com/schmittjoh/serializer/issues/14) -- Strange caching-error [\#13](https://github.com/schmittjoh/serializer/issues/13) -- handling of plain array [\#10](https://github.com/schmittjoh/serializer/issues/10) -- Unsupported format doesn't throw exception anymore [\#8](https://github.com/schmittjoh/serializer/issues/8) -**Merged pull requests:** -- Fix typo [\#32](https://github.com/schmittjoh/serializer/pull/32) ([inanimatt](https://github.com/inanimatt)) -- Fixed the serialization of pluralized form errors [\#31](https://github.com/schmittjoh/serializer/pull/31) ([stof](https://github.com/stof)) -- Extract json specific logic from GenericSerializationVisitor [\#29](https://github.com/schmittjoh/serializer/pull/29) ([adrienbrault](https://github.com/adrienbrault)) -- \[Serializer\] Misc cleanup [\#27](https://github.com/schmittjoh/serializer/pull/27) ([vicb](https://github.com/vicb)) -- \[Builder\] Add ability to include if metadata [\#25](https://github.com/schmittjoh/serializer/pull/25) ([vicb](https://github.com/vicb)) -- Fix DateTimeZone issue when using the DateTime type [\#23](https://github.com/schmittjoh/serializer/pull/23) ([colinmorelli](https://github.com/colinmorelli)) -- Wrong exception message for parsing datetime [\#21](https://github.com/schmittjoh/serializer/pull/21) ([nickelc](https://github.com/nickelc)) -- Fixed typo in doc/reference/annotations.rst [\#16](https://github.com/schmittjoh/serializer/pull/16) ([iambrosi](https://github.com/iambrosi)) -- Typecast when serializing primitive types [\#15](https://github.com/schmittjoh/serializer/pull/15) ([baldurrensch](https://github.com/baldurrensch)) -- add check and helpful exception message on inconsistent type situation [\#12](https://github.com/schmittjoh/serializer/pull/12) ([dbu](https://github.com/dbu)) -- Dispatch pre-serialization event before handling data to have ability change type in listener [\#7](https://github.com/schmittjoh/serializer/pull/7) ([megazoll](https://github.com/megazoll)) -- Fix tests running in different environments [\#6](https://github.com/schmittjoh/serializer/pull/6) ([megazoll](https://github.com/megazoll)) -- Add DateInterval serialization to DateHandler formerly DateTimeHandler [\#5](https://github.com/schmittjoh/serializer/pull/5) ([rpg600](https://github.com/rpg600)) -- WIP Navigator context [\#3](https://github.com/schmittjoh/serializer/pull/3) ([adrienbrault](https://github.com/adrienbrault)) -- Update src/JMS/Serializer/Construction/DoctrineObjectConstructor.php [\#2](https://github.com/schmittjoh/serializer/pull/2) ([robocoder](https://github.com/robocoder)) -- Filter out non-identifiers from $data before calling find\(\) [\#1](https://github.com/schmittjoh/serializer/pull/1) ([robocoder](https://github.com/robocoder)) - - - -\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/README.md b/README.md index b2085cf1a..96f787af4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # jms/serializer - [![Build status][Master image]][Master] [![Coverage Status][Master coverage image]][Master coverage] diff --git a/UPGRADING.md b/UPGRADING.md index 3255b8a06..7f023908f 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,7 +6,7 @@ Upgrading from 2.x to 3.x should require almost no effort. The only change is the revert of "deeper branch group exclusion strategy" introduced in 2.0.0 and now reverted as it was in 1.x. If you are not using this feature, then upgrading requires no changes at all. -The deprecations introduced in 2.x are still present in 3.0.0, said features are most likley to be removed in an next major. +The deprecations introduced in 2.x are still present in 3.0.0, said features are most likely to be removed in the next major version. From 1.x to 3.0.0 ================= @@ -81,7 +81,7 @@ If you are on version `1.x`, it is suggested to migrate directly to `3.0.0` (sin -**Deprecations** (will be removed in 3.0) +**Deprecations** (will be removed in 4.0) - `JsonSerializationVisitor::setData` will be removed, use `::visitProperty(new StaticPropertyMetadata('', 'name', 'value'), 'value')` instead diff --git a/composer.json b/composer.json index 5876f0376..7c0f42b5b 100644 --- a/composer.json +++ b/composer.json @@ -16,38 +16,36 @@ } ], "require": { - "php": "^7.2", - "jms/metadata": "^2.0", + "php": "^7.2||^8.0", "doctrine/annotations": "^1.0", "doctrine/instantiator": "^1.0.3", - "hoa/compiler": "^3.17.08.08" + "doctrine/lexer": "^1.1", + "jms/metadata": "^2.0", + "phpstan/phpdoc-parser": "^0.4" }, "suggest": { - "symfony/yaml": "Required if you'd like to use the YAML metadata format.", + "doctrine/cache": "Required if you like to use cache functionality.", "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", - "doctrine/cache": "Required if you like to use cache functionality." + "symfony/yaml": "Required if you'd like to use the YAML metadata format." }, "require-dev": { "ext-pdo_sqlite": "*", - "twig/twig": "~1.34|~2.4", + "doctrine/coding-standard": "^8.1", "doctrine/orm": "~2.1", - "jackalope/jackalope-doctrine-dbal": "^1.1.5", + "doctrine/persistence": "^1.3.3|^2.0|^3.0", "doctrine/phpcr-odm": "^1.3|^2.0", + "jackalope/jackalope-doctrine-dbal": "^1.1.5", + "ocramius/proxy-manager": "^1.0|^2.0", + "phpunit/phpunit": "^8.0||^9.0", "psr/container": "^1.0", - "symfony/dependency-injection": "^3.0|^4.0", - "symfony/yaml": "^3.3|^4.0", - "symfony/translation": "^3.0|^4.0", - "symfony/validator": "^3.1.9|^4.0", - "symfony/form": "^3.0|^4.0", - "symfony/filesystem": "^3.0|^4.0", - "symfony/expression-language": "^3.0|^4.0", - "phpunit/phpunit": "^7.5||^8.0", - "doctrine/coding-standard": "^5.0" - }, - "conflict": { - "hoa/core": "*", - "hoa/consistency": "<1.17.05.02", - "hoa/iterator": "<2.16.03.15" + "symfony/dependency-injection": "^3.0|^4.0|^5.0", + "symfony/expression-language": "^3.0|^4.0|^5.0", + "symfony/filesystem": "^3.0|^4.0|^5.0", + "symfony/form": "^3.0|^4.0|^5.0", + "symfony/translation": "^3.0|^4.0|^5.0", + "symfony/validator": "^3.1.9|^4.0|^5.0", + "symfony/yaml": "^3.3|^4.0|^5.0", + "twig/twig": "~1.34|~2.4|^3.0" }, "autoload": { "psr-4": { @@ -59,9 +57,12 @@ "JMS\\Serializer\\Tests\\": "tests/" } }, + "config": { + "sort-packages": true + }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.10-dev" } } } diff --git a/doc/conf.py b/doc/conf.py index 832dbe106..3f2278480 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -6,7 +6,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sensio.sphinx.refinclude', 'sensio.sphinx.configurationblock', 'sensio.sphinx.phpcode'] +extensions = ['sensio.sphinx.configurationblock', 'sensio.sphinx.phpcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/doc/configuration.rst b/doc/configuration.rst index 48d86e580..aabe95662 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -75,7 +75,7 @@ are located:: The serializer would expect the metadata files to be named like the fully qualified class names where all ``\`` are replaced with ``.``. So, if you class would be named ``Vendor\Package\Foo``, the metadata file would need to be located -at ``$someDir/Vendor.Package.Foo.(xml|yml)``. For more information, see the :doc:`reference `. +at ``$someDir/Vendor.Package.Foo.(xml|yml)``. If not found, ``$someDir/Vendor.Package.(xml|yml)`` will be tried, then ``$someDir/Vendor.Package.(xml|yml)`` and so on. For more information, see the :doc:`reference `. Setting a default SerializationContext factory ---------------------------------------------- diff --git a/doc/cookbook/exclusion_strategies.rst b/doc/cookbook/exclusion_strategies.rst index 066dabcf2..03fd95245 100644 --- a/doc/cookbook/exclusion_strategies.rst +++ b/doc/cookbook/exclusion_strategies.rst @@ -272,12 +272,16 @@ Dynamic exclusion strategy If the previous exclusion strategies are not enough, is possible to use the ``ExpressionLanguageExclusionStrategy`` that uses the `symfony expression language`_ to allow a more sophisticated exclusion strategies using ``@Exclude(if="expression")`` and ``@Expose(if="expression")`` methods. +This also works on class level, but is only evaluated during ``serialze`` and does not have any effect during ``deserialze``. .. code-block :: php ") + */ + public $accounts; + } + + /** + * @Exclude(if="object.expired || !is_granted('view',object)") + */ + class Account + { + /** + * @Type("boolean") + */ + public $expired; + } diff --git a/doc/handlers.rst b/doc/handlers.rst index 441ad8f7e..a7c1b717c 100644 --- a/doc/handlers.rst +++ b/doc/handlers.rst @@ -78,3 +78,8 @@ Also, this type of handler is registered via the builder object:: }) ; +Skippable Subscribing Handlers +------------------------------- + +In case you need to be able to fall back to the default deserialization behavior instead of using your custom +handler, you can simply throw a `SkipHandlerException` from you custom handler method to do so. diff --git a/doc/reference/annotations.rst b/doc/reference/annotations.rst index 2ada97776..f75ab3dbf 100644 --- a/doc/reference/annotations.rst +++ b/doc/reference/annotations.rst @@ -1,800 +1,831 @@ -Annotations ------------ - -@ExclusionPolicy -~~~~~~~~~~~~~~~~ -This annotation can be defined on a class to indicate the exclusion strategy -that should be used for the class. - -+----------+----------------------------------------------------------------+ -| Policy | Description | -+==========+================================================================+ -| all | all properties are excluded by default; only properties marked | -| | with @Expose will be serialized/unserialized | -+----------+----------------------------------------------------------------+ -| none | no properties are excluded by default; all properties except | -| | those marked with @Exclude will be serialized/unserialized | -+----------+----------------------------------------------------------------+ - -@Exclude -~~~~~~~~ -This annotation can be defined on a property to indicate that the property should -not be serialized/unserialized. Works only in combination with NoneExclusionPolicy. - -If the ``ExpressionLanguageExclusionStrategy`` exclusion strategy is enabled, will -be possible to use ``@Exclude(if="expression")`` to exclude dynamically a property. - -@Expose -~~~~~~~ -This annotation can be defined on a property to indicate that the property should -be serialized/unserialized. Works only in combination with AllExclusionPolicy. - -If the ``ExpressionLanguageExclusionStrategy`` exclusion strategy is enabled, will -be possible to use ``@Expose(if="expression")`` to expose dynamically a property. - -@SkipWhenEmpty -~~~~~~~~~~~~~~ -This annotation can be defined on a property to indicate that the property should -not be serialized if the result will be "empty". - -Works option works only when serializing. - -@SerializedName -~~~~~~~~~~~~~~~ -This annotation can be defined on a property to define the serialized name for a -property. If this is not defined, the property will be translated from camel-case -to a lower-cased underscored name, e.g. camelCase -> camel_case. - -Note that this annotation is not used when you're using any other naming -strategy than the default configuration (which includes the -``SerializedNameAnnotationStrategy``). In order to re-enable the annotation, you -will need to wrap your custom strategy with the ``SerializedNameAnnotationStrategy``. - -.. code-block :: php - - setPropertyNamingStrategy( - new \JMS\Serializer\Naming\SerializedNameAnnotationStrategy( - new \JMS\Serializer\Naming\IdenticalPropertyNamingStrategy() - ) - ) - ->build(); - -@Since -~~~~~~ -This annotation can be defined on a property to specify starting from which -version this property is available. If an earlier version is serialized, then -this property is excluded automatically. The version must be in a format that is -understood by PHP's ``version_compare`` function. - -@Until -~~~~~~ -This annotation can be defined on a property to specify until which version this -property was available. If a later version is serialized, then this property is -excluded automatically. The version must be in a format that is understood by -PHP's ``version_compare`` function. - -@Groups -~~~~~~~ -This annotation can be defined on a property to specify if the property -should be serialized when only serializing specific groups (see -:doc:`../cookbook/exclusion_strategies`). - -@MaxDepth -~~~~~~~~~ -This annotation can be defined on a property to limit the depth to which the -content will be serialized. It is very useful when a property will contain a -large object graph. - -@AccessType -~~~~~~~~~~~ -This annotation can be defined on a property, or a class to specify in which way -the properties should be accessed. By default, the serializer will retrieve, or -set the value via reflection, but you may change this to use a public method instead: - -.. code-block :: php - - name; - } - - public function setName($name) - { - $this->name = trim($name); - } - } - -@Accessor -~~~~~~~~~ -This annotation can be defined on a property to specify which public method should -be called to retrieve, or set the value of the given property: - -.. code-block :: php - - name); - } - - public function setName($name) - { - $this->name = $name; - } - } - -.. note :: - - If you need only to serialize your data, you can avoid providing a setter by - setting the property as read-only using the ``@ReadOnly`` annotation. - -@AccessorOrder -~~~~~~~~~~~~~~ -This annotation can be defined on a class to control the order of properties. By -default the order is undefined, but you may change it to either "alphabetical", or -"custom". - -.. code-block :: php - - lastName; - } - - public function getFirstName() - { - return $this->firstName; - } - } - -In this example: - -- ``id`` is exposed using the object reflection. -- ``lastName`` is exposed using the ``getLastName`` getter method. -- ``firstName`` is exposed using the ``object.getFirstName()`` expression (``exp`` can contain any valid symfony expression). - - -``@VirtualProperty()`` can also have an optional property ``name``, used to define the internal property name -(for sorting proposes as example). When not specified, it defaults to the method name with the "get" prefix removed. - -.. note :: - - This only works for serialization and is completely ignored during deserialization. - -@Inline -~~~~~~~ -This annotation can be defined on a property to indicate that the data of the property -should be inlined. - -**Note**: AccessorOrder will be using the name of the property to determine the order. - -@ReadOnly -~~~~~~~~~ -This annotation can be defined on a property to indicate that the data of the property -is read only and cannot be set during deserialization. - -A property can be marked as non read only with ``@ReadOnly(false)`` annotation (useful when a class is marked as read only). - -@PreSerialize -~~~~~~~~~~~~~ -This annotation can be defined on a method which is supposed to be called before -the serialization of the object starts. - -@PostSerialize -~~~~~~~~~~~~~~ -This annotation can be defined on a method which is then called directly after the -object has been serialized. - -@PostDeserialize -~~~~~~~~~~~~~~~~ -This annotation can be defined on a method which is supposed to be called after -the object has been deserialized. - -@Discriminator -~~~~~~~~~~~~~~ - -.. versionadded : 0.12 - @Discriminator was added - -This annotation allows serialization/deserialization of relations which are polymorphic, but -where a common base class exists. The ``@Discriminator`` annotation has to be applied -to the least super type:: - - /** - * @Discriminator(field = "type", disabled = false, map = {"car": "Car", "moped": "Moped"}, groups={"foo", "bar"}) - */ - abstract class Vehicle { } - class Car extends Vehicle { } - class Moped extends Vehicle { } - - -.. note :: - - `groups` is optional and is used as exclusion policy. - -@Type -~~~~~ -This annotation can be defined on a property to specify the type of that property. -For deserialization, this annotation must be defined. -The ``@Type`` annotation can have parameters and parameters can be used by serialization/deserialization -handlers to enhance the serialization or deserialization result; for example, you may want to -force a certain format to be used for serializing DateTime types and specifying at the same time a different format -used when deserializing them. - -Available Types: - -+----------------------------------------------------------+--------------------------------------------------+ -| Type | Description | -+==========================================================+==================================================+ -| boolean or bool | Primitive boolean | -+----------------------------------------------------------+--------------------------------------------------+ -| integer or int | Primitive integer | -+----------------------------------------------------------+--------------------------------------------------+ -| double or float | Primitive double | -+----------------------------------------------------------+--------------------------------------------------+ -| string | Primitive string | -+----------------------------------------------------------+--------------------------------------------------+ -| array | An array with arbitrary keys, and values. | -+----------------------------------------------------------+--------------------------------------------------+ -| array | A list of type T (T can be any available type). | -| | Examples: | -| | array, array, etc. | -+----------------------------------------------------------+--------------------------------------------------+ -| array | A map of keys of type K to values of type V. | -| | Examples: array, | -| | array, etc. | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTime | PHP's DateTime object (default format*/timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTime<'format'> | PHP's DateTime object (custom format/default | -| | timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTime<'format', 'zone'> | PHP's DateTime object (custom format/timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTime<'format', 'zone', 'deserializeFormat'> | PHP's DateTime object (custom format/timezone, | -| | deserialize format). If you do not want to | -| | specify a specific timezone, use an empty | -| | string (''). | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTimeImmutable | PHP's DateTimeImmutable object (default format*/ | -| | timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTimeImmutable<'format'> | PHP's DateTimeImmutable object (custom format/ | -| | default timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTimeImmutable<'format', 'zone'> | PHP's DateTimeImmutable object (custom format/ | -| | timezone) | -+----------------------------------------------------------+--------------------------------------------------+ -| DateTimeImmutable<'format', 'zone', 'deserializeFormat'> | PHP's DateTimeImmutable object (custom format/ | -| | timezone/deserialize format). If you do not want | -| | to specify a specific timezone, use an empty | -| | string (''). | -+----------------------------------------------------------+--------------------------------------------------+ -| DateInterval | PHP's DateInterval object using ISO 8601 format | -+----------------------------------------------------------+--------------------------------------------------+ -| T | Where T is a fully qualified class name. | -+----------------------------------------------------------+--------------------------------------------------+ -| iterable | Similar to array. Will always be deserialized | -| | into array as implementation info is lost during | -| | serialization. | -+----------------------------------------------------------+--------------------------------------------------+ -| iterable | Similar to array. Will always be deserialized | -| | into array as implementation info is lost during | -| | serialization. | -+----------------------------------------------------------+--------------------------------------------------+ -| iterable | Similar to array. Will always be | -| | deserialized into array as implementation info | -| | is lost during serialization. | -+----------------------------------------------------------+--------------------------------------------------+ -| ArrayCollection | Similar to array, but will be deserialized | -| | into Doctrine's ArrayCollection class. | -+----------------------------------------------------------+--------------------------------------------------+ -| ArrayCollection | Similar to array, but will be deserialized | -| | into Doctrine's ArrayCollection class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Generator | Similar to array, but will be deserialized | -| | into Generator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Generator | Similar to array, but will be deserialized | -| | into Generator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Generator | Similar to array, but will be deserialized | -| | into Generator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| ArrayIterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| ArrayIterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| ArrayIterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Iterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Iterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ -| Iterator | Similar to array, but will be deserialized | -| | into ArrayIterator class. | -+----------------------------------------------------------+--------------------------------------------------+ - -(*) If the standalone jms/serializer is used then default format is `\DateTime::ISO8601` (which is not compatible with ISO-8601 despite the name). For jms/serializer-bundle the default format is `\DateTime::ATOM` (the real ISO-8601 format) but it can be changed in `configuration`_. - -Examples: - -.. code-block :: php - - ") - */ - private $comments; - - /** - * @Type("string") - */ - private $title; - - /** - * @Type("MyNamespace\Author") - */ - private $author; - - /** - * @Type("DateTime") - */ - private $startAt; - - /** - * @Type("DateTime<'Y-m-d'>") - */ - private $endAt; - - /** - * @Type("DateTimeImmutable") - */ - private $createdAt; - - /** - * @Type("DateTimeImmutable<'Y-m-d'>") - */ - private $updatedAt; - - /** - * @Type("boolean") - */ - private $published; - - /** - * @Type("array") - */ - private $keyValueStore; - } - -.. _configuration: https://jmsyst.com/bundles/JMSSerializerBundle/master/configuration#configuration-block-2-0 - -@XmlRoot -~~~~~~~~ -This allows you to specify the name of the top-level element. - -.. code-block :: php - - - - - -.. note :: - - @XmlRoot only applies to the root element, but is for example not taken into - account for collections. You can define the entry name for collections using - @XmlList, or @XmlMap. - -@XmlAttribute -~~~~~~~~~~~~~ -This allows you to mark properties which should be set as attributes, -and not as child elements. - -.. code-block :: php - - - - - - -@XmlDiscriminator -~~~~~~~~~~~~~~~~~ -This annotation allows to modify the behaviour of @Discriminator regarding handling of XML. - - -Available Options: - -+-------------------------------------+--------------------------------------------------+ -| Type | Description | -+=====================================+==================================================+ -| attribute | use an attribute instead of a child node | -+-------------------------------------+--------------------------------------------------+ -| cdata | render child node content with or without cdata | -+-------------------------------------+--------------------------------------------------+ -| namespace | render child node using the specified namespace | -+-------------------------------------+--------------------------------------------------+ - -Example for "attribute": - -.. code-block :: php - - - - -Example for "cdata": - -.. code-block :: php - - car - - -@XmlValue -~~~~~~~~~ -This allows you to mark properties which should be set as the value of the -current element. Note that this has the limitation that any additional -properties of that object must have the @XmlAttribute annotation. -XMlValue also has property cdata. Which has the same meaning as the one in -XMLElement. - -.. code-block :: php - - 1.23 - -@XmlList -~~~~~~~~ -This allows you to define several properties of how arrays should be -serialized. This is very similar to @XmlMap, and should be used if the -keys of the array are not important. - -.. code-block :: php - - text = $text; - } - } - -Resulting XML: - -.. code-block :: xml - - - - - - - - - - -You can also specify the entry tag namespace using the ``namespace`` attribute (``@XmlList(inline = true, entry = "comment", namespace="http://www.example.com/ns")``). - -@XmlMap -~~~~~~~ -Similar to @XmlList, but the keys of the array are meaningful. - -@XmlKeyValuePairs -~~~~~~~~~~~~~~~~~ -This allows you to use the keys of an array as xml tags. - -.. note :: - - When a key is an invalid xml tag name (e.g. 1_foo) the tag name *entry* will be used instead of the key. - -@XmlAttributeMap -~~~~~~~~~~~~~~~~ - -This is similar to the @XmlKeyValuePairs, but instead of creating child elements, it creates attributes. - -.. code-block :: php - - 'firstname', - 'value' => 'Adrien', - ); - } - -Resulting XML: - -.. code-block :: xml - - - -@XmlElement -~~~~~~~~~~~ -This annotation can be defined on a property to add additional xml serialization/deserialization properties. - -.. code-block :: php - - my_id - -@XmlNamespace -~~~~~~~~~~~~~ -This annotation allows you to specify Xml namespace/s and prefix used. - -.. code-block :: php - - - - - - - +Annotations +----------- + +@ExclusionPolicy +~~~~~~~~~~~~~~~~ +This annotation can be defined on a class to indicate the exclusion strategy +that should be used for the class. + ++----------+----------------------------------------------------------------+ +| Policy | Description | ++==========+================================================================+ +| all | all properties are excluded by default; only properties marked | +| | with @Expose will be serialized/unserialized | ++----------+----------------------------------------------------------------+ +| none | no properties are excluded by default; all properties except | +| | those marked with @Exclude will be serialized/unserialized | ++----------+----------------------------------------------------------------+ + +@Exclude +~~~~~~~~ +This annotation can be defined on a property or a class to indicate that the property or class +should not be serialized/unserialized. Works only in combination with NoneExclusionPolicy. + +If the ``ExpressionLanguageExclusionStrategy`` exclusion strategy is enabled, it will +be possible to use ``@Exclude(if="expression")`` to exclude dynamically a property +or an object if used on class level. + +@Expose +~~~~~~~ +This annotation can be defined on a property to indicate that the property should +be serialized/unserialized. Works only in combination with AllExclusionPolicy. + +If the ``ExpressionLanguageExclusionStrategy`` exclusion strategy is enabled, will +be possible to use ``@Expose(if="expression")`` to expose dynamically a property. + +@SkipWhenEmpty +~~~~~~~~~~~~~~ +This annotation can be defined on a property to indicate that the property should +not be serialized if the result will be "empty". + +Works option works only when serializing. + +@SerializedName +~~~~~~~~~~~~~~~ +This annotation can be defined on a property to define the serialized name for a +property. If this is not defined, the property will be translated from camel-case +to a lower-cased underscored name, e.g. camelCase -> camel_case. + +Note that this annotation is not used when you're using any other naming +strategy than the default configuration (which includes the +``SerializedNameAnnotationStrategy``). In order to re-enable the annotation, you +will need to wrap your custom strategy with the ``SerializedNameAnnotationStrategy``. + +.. code-block :: php + + setPropertyNamingStrategy( + new \JMS\Serializer\Naming\SerializedNameAnnotationStrategy( + new \JMS\Serializer\Naming\IdenticalPropertyNamingStrategy() + ) + ) + ->build(); + +@Since +~~~~~~ +This annotation can be defined on a property to specify starting from which +version this property is available. If an earlier version is serialized, then +this property is excluded automatically. The version must be in a format that is +understood by PHP's ``version_compare`` function. + +@Until +~~~~~~ +This annotation can be defined on a property to specify until which version this +property was available. If a later version is serialized, then this property is +excluded automatically. The version must be in a format that is understood by +PHP's ``version_compare`` function. + +@Groups +~~~~~~~ +This annotation can be defined on a property to specify if the property +should be serialized when only serializing specific groups (see +:doc:`../cookbook/exclusion_strategies`). + +@MaxDepth +~~~~~~~~~ +This annotation can be defined on a property to limit the depth to which the +content will be serialized. It is very useful when a property will contain a +large object graph. + +@AccessType +~~~~~~~~~~~ +This annotation can be defined on a property, or a class to specify in which way +the properties should be accessed. By default, the serializer will retrieve, or +set the value via reflection, but you may change this to use a public method instead: + +.. code-block :: php + + name; + } + + public function setName($name) + { + $this->name = trim($name); + } + } + +@Accessor +~~~~~~~~~ +This annotation can be defined on a property to specify which public method should +be called to retrieve, or set the value of the given property: + +.. code-block :: php + + name); + } + + public function setName($name) + { + $this->name = $name; + } + } + +.. note :: + + If you need only to serialize your data, you can avoid providing a setter by + setting the property as read-only using the ``@ReadOnly`` annotation. + +@AccessorOrder +~~~~~~~~~~~~~~ +This annotation can be defined on a class to control the order of properties. By +default the order is undefined, but you may change it to either "alphabetical", or +"custom". + +.. code-block :: php + + lastName; + } + + public function getFirstName() + { + return $this->firstName; + } + } + +In this example: + +- ``id`` is exposed using the object reflection. +- ``lastName`` is exposed using the ``getLastName`` getter method. +- ``firstName`` is exposed using the ``object.getFirstName()`` expression (``exp`` can contain any valid symfony expression). + + +``@VirtualProperty()`` can also have an optional property ``name``, used to define the internal property name +(for sorting proposes as example). When not specified, it defaults to the method name with the "get" prefix removed. + +.. note :: + + This only works for serialization and is completely ignored during deserialization. + +@Inline +~~~~~~~ +This annotation can be defined on a property to indicate that the data of the property +should be inlined. + +**Note**: AccessorOrder will be using the name of the property to determine the order. + +@ReadOnly +~~~~~~~~~ +This annotation can be defined on a property to indicate that the data of the property +is read only and cannot be set during deserialization. + +A property can be marked as non read only with ``@ReadOnly(false)`` annotation (useful when a class is marked as read only). + +@PreSerialize +~~~~~~~~~~~~~ +This annotation can be defined on a method which is supposed to be called before +the serialization of the object starts. + +@PostSerialize +~~~~~~~~~~~~~~ +This annotation can be defined on a method which is then called directly after the +object has been serialized. + +@PostDeserialize +~~~~~~~~~~~~~~~~ +This annotation can be defined on a method which is supposed to be called after +the object has been deserialized. + +@Discriminator +~~~~~~~~~~~~~~ + +.. versionadded : 0.12 + @Discriminator was added + +This annotation allows serialization/deserialization of relations which are polymorphic, but +where a common base class exists. The ``@Discriminator`` annotation has to be applied +to the least super type:: + + /** + * @Discriminator(field = "type", disabled = false, map = {"car": "Car", "moped": "Moped"}, groups={"foo", "bar"}) + */ + abstract class Vehicle { } + class Car extends Vehicle { } + class Moped extends Vehicle { } + + +.. note :: + + `groups` is optional and is used as exclusion policy. + +@Type +~~~~~ +This annotation can be defined on a property to specify the type of that property. +For deserialization, this annotation must be defined. +The ``@Type`` annotation can have parameters and parameters can be used by serialization/deserialization +handlers to enhance the serialization or deserialization result; for example, you may want to +force a certain format to be used for serializing DateTime types and specifying at the same time a different format +used when deserializing them. + +Available Types: + ++------------------------------------------------------------+--------------------------------------------------+ +| Type | Description | ++============================================================+==================================================+ +| boolean or bool | Primitive boolean | ++------------------------------------------------------------+--------------------------------------------------+ +| integer or int | Primitive integer | ++------------------------------------------------------------+--------------------------------------------------+ +| double or float | Primitive double | ++------------------------------------------------------------+--------------------------------------------------+ +| string | Primitive string | ++------------------------------------------------------------+--------------------------------------------------+ +| array | An array with arbitrary keys, and values. | ++------------------------------------------------------------+--------------------------------------------------+ +| array | A list of type T (T can be any available type). | +| | Examples: | +| | array, array, etc. | ++------------------------------------------------------------+--------------------------------------------------+ +| array | A map of keys of type K to values of type V. | +| | Examples: array, | +| | array, etc. | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTime | PHP's DateTime object (default format*/timezone) | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTime<'format'> | PHP's DateTime object (custom format/default | +| | timezone). | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTime<'format', 'zone'> | PHP's DateTime object (custom format/timezone) | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTime<'format', 'zone', 'deserializeFormats'> | PHP's DateTime object (custom format/timezone, | +| | deserialize format). If you do not want to | +| | specify a specific timezone, use an empty | +| | string (''). DeserializeFormats can either be a | +| | string or an array of string. | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeImmutable | PHP's DateTimeImmutable object (default format*/ | +| | timezone). | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeImmutable<'format'> | PHP's DateTimeImmutable object (custom format/ | +| | default timezone) | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeImmutable<'format', 'zone'> | PHP's DateTimeImmutable object (custom format/ | +| | timezone) | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeImmutable<'format', 'zone', 'deserializeFormats'> | PHP's DateTimeImmutable object (custom format/ | +| | timezone/deserialize format). If you do not want | +| | to specify a specific timezone, use an empty | +| | string (''). DeserializeFormats can either be a | +| | string or an array of string. | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeInterface | PHP's DateTimeImmutable object (default format*/ | +| | timezone). | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeInterface<'format'> | PHP's DateTimeImmutable object (custom format/ | +| | default timezone) | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeInterface<'format', 'zone'> | PHP's DateTimeImmutable object (custom format/ | +| | timezone) | ++------------------------------------------------------------+--------------------------------------------------+ +| DateTimeInterface<'format', 'zone', 'deserializeFormats'> | PHP's DateTimeImmutable object (custom format/ | +| | timezone/deserialize format). If you do not want | +| | to specify a specific timezone, use an empty | +| | string (''). DeserializeFormats can either be a | +| | string or an array of string. | ++------------------------------------------------------------+--------------------------------------------------+ +| DateInterval | PHP's DateInterval object using ISO 8601 format | ++------------------------------------------------------------+--------------------------------------------------+ +| T | Where T is a fully qualified class name. | ++------------------------------------------------------------+--------------------------------------------------+ +| iterable | Similar to array. Will always be deserialized | +| | into array as implementation info is lost during | +| | serialization. | ++------------------------------------------------------------+--------------------------------------------------+ +| iterable | Similar to array. Will always be deserialized | +| | into array as implementation info is lost during | +| | serialization. | ++------------------------------------------------------------+--------------------------------------------------+ +| iterable | Similar to array. Will always be | +| | deserialized into array as implementation info | +| | is lost during serialization. | ++------------------------------------------------------------+--------------------------------------------------+ +| ArrayCollection | Similar to array, but will be deserialized | +| | into Doctrine's ArrayCollection class. | ++------------------------------------------------------------+--------------------------------------------------+ +| ArrayCollection | Similar to array, but will be deserialized | +| | into Doctrine's ArrayCollection class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Generator | Similar to array, but will be deserialized | +| | into Generator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Generator | Similar to array, but will be deserialized | +| | into Generator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Generator | Similar to array, but will be deserialized | +| | into Generator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| ArrayIterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| ArrayIterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| ArrayIterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Iterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Iterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ +| Iterator | Similar to array, but will be deserialized | +| | into ArrayIterator class. | ++------------------------------------------------------------+--------------------------------------------------+ + +(*) If the standalone jms/serializer is used then default format is `\DateTime::ISO8601` (which is not compatible with ISO-8601 despite the name). For jms/serializer-bundle the default format is `\DateTime::ATOM` (the real ISO-8601 format) but it can be changed in `configuration`_. + +(**) The key type K for array-linke formats as ``array``. ``ArrayCollection``, ``iterable``, etc., is only used for deserialization, +for serializaiton is treated as ``string``. + +Examples: + +.. code-block :: php + + ") + */ + private $comments; + + /** + * @Type("string") + */ + private $title; + + /** + * @Type("MyNamespace\Author") + */ + private $author; + + /** + * @Type("DateTime") + */ + private $startAt; + + /** + * @Type("DateTime<'Y-m-d'>") + */ + private $endAt; + + /** + * @Type("DateTime<'Y-m-d', '', ['Y-m-d', 'Y/m/d']>") + */ + private $publishedAt; + + /** + * @Type("DateTimeImmutable") + */ + private $createdAt; + + /** + * @Type("DateTimeImmutable<'Y-m-d'>") + */ + private $updatedAt; + + /** + * @Type("DateTimeImmutable<'Y-m-d', '', ['Y-m-d', 'Y/m/d']>") + */ + private $deletedAt; + + /** + * @Type("boolean") + */ + private $published; + + /** + * @Type("array") + */ + private $keyValueStore; + } + +.. _configuration: https://jmsyst.com/bundles/JMSSerializerBundle/master/configuration#configuration-block-2-0 + +@XmlRoot +~~~~~~~~ +This allows you to specify the name of the top-level element. + +.. code-block :: php + + + + + +.. note :: + + @XmlRoot only applies to the root element, but is for example not taken into + account for collections. You can define the entry name for collections using + @XmlList, or @XmlMap. + +@XmlAttribute +~~~~~~~~~~~~~ +This allows you to mark properties which should be set as attributes, +and not as child elements. + +.. code-block :: php + + + + + + +@XmlDiscriminator +~~~~~~~~~~~~~~~~~ +This annotation allows to modify the behaviour of @Discriminator regarding handling of XML. + + +Available Options: + ++-------------------------------------+--------------------------------------------------+ +| Type | Description | ++=====================================+==================================================+ +| attribute | use an attribute instead of a child node | ++-------------------------------------+--------------------------------------------------+ +| cdata | render child node content with or without cdata | ++-------------------------------------+--------------------------------------------------+ +| namespace | render child node using the specified namespace | ++-------------------------------------+--------------------------------------------------+ + +Example for "attribute": + +.. code-block :: php + + + + +Example for "cdata": + +.. code-block :: php + + car + + +@XmlValue +~~~~~~~~~ +This allows you to mark properties which should be set as the value of the +current element. Note that this has the limitation that any additional +properties of that object must have the @XmlAttribute annotation. +XMlValue also has property cdata. Which has the same meaning as the one in +XMLElement. + +.. code-block :: php + + 1.23 + +@XmlList +~~~~~~~~ +This allows you to define several properties of how arrays should be +serialized. This is very similar to @XmlMap, and should be used if the +keys of the array are not important. + +.. code-block :: php + + text = $text; + } + } + +Resulting XML: + +.. code-block :: xml + + + + + + + + + + +You can also specify the entry tag namespace using the ``namespace`` attribute (``@XmlList(inline = true, entry = "comment", namespace="http://www.example.com/ns")``). + +@XmlMap +~~~~~~~ +Similar to @XmlList, but the keys of the array are meaningful. + +@XmlKeyValuePairs +~~~~~~~~~~~~~~~~~ +This allows you to use the keys of an array as xml tags. + +.. note :: + + When a key is an invalid xml tag name (e.g. 1_foo) the tag name *entry* will be used instead of the key. + +@XmlAttributeMap +~~~~~~~~~~~~~~~~ + +This is similar to the @XmlKeyValuePairs, but instead of creating child elements, it creates attributes. + +.. code-block :: php + + 'firstname', + 'value' => 'Adrien', + ); + } + +Resulting XML: + +.. code-block :: xml + + + +@XmlElement +~~~~~~~~~~~ +This annotation can be defined on a property to add additional xml serialization/deserialization properties. + +.. code-block :: php + + my_id + +@XmlNamespace +~~~~~~~~~~~~~ +This annotation allows you to specify Xml namespace/s and prefix used. + +.. code-block :: php + + + + + + + diff --git a/doc/reference/xml_reference.rst b/doc/reference/xml_reference.rst index 5f8d8b2f8..239b8310b 100644 --- a/doc/reference/xml_reference.rst +++ b/doc/reference/xml_reference.rst @@ -6,7 +6,7 @@ XML Reference diff --git a/doc/reference/yml_reference.rst b/doc/reference/yml_reference.rst index 90bd669ab..b7ba44ddc 100644 --- a/doc/reference/yml_reference.rst +++ b/doc/reference/yml_reference.rst @@ -8,6 +8,7 @@ YAML Reference xml_root_name: foobar xml_root_namespace: http://your.default.namespace exclude: true + exclude_if: expr read_only: false access_type: public_method # defaults to property accessor_order: custom diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 8771d3347..26031d5b9 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -20,9 +20,10 @@ - - - + + + + @@ -30,9 +31,9 @@ - - - + + + @@ -71,11 +72,14 @@ - - - - - + + tests/* + + + tests/* + + + tests/* @@ -90,9 +94,6 @@ tests/* - - tests/* - tests/* diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6a17b2a31..ae05c3cea 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -13,6 +13,7 @@ + diff --git a/src/Accessor/DefaultAccessorStrategy.php b/src/Accessor/DefaultAccessorStrategy.php index 874f9c511..c3e45d819 100644 --- a/src/Accessor/DefaultAccessorStrategy.php +++ b/src/Accessor/DefaultAccessorStrategy.php @@ -45,7 +45,6 @@ public function __construct(?ExpressionEvaluatorInterface $evaluator = null) $this->evaluator = $evaluator; } - /** * {@inheritdoc} */ @@ -65,33 +64,34 @@ public function getValue(object $object, PropertyMetadata $metadata, Serializati if (($metadata->expression instanceof Expression) && ($this->evaluator instanceof CompilableExpressionEvaluatorInterface)) { return $this->evaluator->evaluateParsed($metadata->expression, $variables); } + return $this->evaluator->evaluate($metadata->expression, $variables); } - if (null === $metadata->getter) { - if (!isset($this->readAccessors[$metadata->class])) { - if (true === $metadata->forceReflectionAccess) { - $this->readAccessors[$metadata->class] = function ($o, $name) use ($metadata) { - $ref = $this->propertyReflectionCache[$metadata->class][$name] ?? null; - if (null === $ref) { - $ref = new \ReflectionProperty($metadata->class, $name); - $ref->setAccessible(true); - $this->propertyReflectionCache[$metadata->class][$name] = $ref; - } - - return $ref->getValue($o); - }; - } else { - $this->readAccessors[$metadata->class] = \Closure::bind(static function ($o, $name) { - return $o->$name; - }, null, $metadata->class); - } + if (null !== $metadata->getter) { + return $object->{$metadata->getter}(); + } + + if ($metadata->forceReflectionAccess) { + $ref = $this->propertyReflectionCache[$metadata->class][$metadata->name] ?? null; + if (null === $ref) { + $ref = new \ReflectionProperty($metadata->class, $metadata->name); + $ref->setAccessible(true); + $this->propertyReflectionCache[$metadata->class][$metadata->name] = $ref; } - return $this->readAccessors[$metadata->class]($object, $metadata->name); + return $ref->getValue($object); } - return $object->{$metadata->getter}(); + $accessor = $this->readAccessors[$metadata->class] ?? null; + if (null === $accessor) { + $accessor = \Closure::bind(static function ($o, $name) { + return $o->$name; + }, null, $metadata->class); + $this->readAccessors[$metadata->class] = $accessor; + } + + return $accessor($object, $metadata->name); } /** @@ -103,30 +103,33 @@ public function setValue(object $object, $value, PropertyMetadata $metadata, Des throw new LogicException(sprintf('%s on %s is read only.', $metadata->name, $metadata->class)); } - if (null === $metadata->setter) { - if (!isset($this->writeAccessors[$metadata->class])) { - if (true === $metadata->forceReflectionAccess) { - $this->writeAccessors[$metadata->class] = function ($o, $name, $value) use ($metadata): void { - $ref = $this->propertyReflectionCache[$metadata->class][$name] ?? null; - if (null === $ref) { - $ref = new \ReflectionProperty($metadata->class, $name); - $ref->setAccessible(true); - $this->propertyReflectionCache[$metadata->class][$name] = $ref; - } - - $ref->setValue($o, $value); - }; - } else { - $this->writeAccessors[$metadata->class] = \Closure::bind(static function ($o, $name, $value): void { - $o->$name = $value; - }, null, $metadata->class); - } + if (null !== $metadata->setter) { + $object->{$metadata->setter}($value); + + return; + } + + if ($metadata->forceReflectionAccess) { + $ref = $this->propertyReflectionCache[$metadata->class][$metadata->name] ?? null; + if (null === $ref) { + $ref = new \ReflectionProperty($metadata->class, $metadata->name); + $ref->setAccessible(true); + $this->propertyReflectionCache[$metadata->class][$metadata->name] = $ref; } - $this->writeAccessors[$metadata->class]($object, $metadata->name, $value); + $ref->setValue($object, $value); + return; } - $object->{$metadata->setter}($value); + $accessor = $this->writeAccessors[$metadata->class] ?? null; + if (null === $accessor) { + $accessor = \Closure::bind(static function ($o, $name, $value): void { + $o->$name = $value; + }, null, $metadata->class); + $this->writeAccessors[$metadata->class] = $accessor; + } + + $accessor($object, $metadata->name, $value); } } diff --git a/src/Annotation/VirtualProperty.php b/src/Annotation/VirtualProperty.php index fb253ddf6..56e554789 100644 --- a/src/Annotation/VirtualProperty.php +++ b/src/Annotation/VirtualProperty.php @@ -40,6 +40,7 @@ public function __construct(array $data) if (!property_exists(self::class, $key)) { throw new InvalidArgumentException(sprintf('Unknown property "%s" on annotation "%s".', $key, self::class)); } + $this->{$key} = $value; } } diff --git a/src/Builder/DefaultDriverFactory.php b/src/Builder/DefaultDriverFactory.php index 2dd75d1db..2e87f3b33 100644 --- a/src/Builder/DefaultDriverFactory.php +++ b/src/Builder/DefaultDriverFactory.php @@ -7,6 +7,7 @@ use Doctrine\Common\Annotations\Reader; use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface; use JMS\Serializer\Metadata\Driver\AnnotationDriver; +use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver; use JMS\Serializer\Metadata\Driver\XmlDriver; use JMS\Serializer\Metadata\Driver\YamlDriver; use JMS\Serializer\Naming\PropertyNamingStrategyInterface; @@ -45,13 +46,19 @@ public function createDriver(array $metadataDirs, Reader $annotationReader): Dri if (!empty($metadataDirs)) { $fileLocator = new FileLocator($metadataDirs); - return new DriverChain([ + $driver = new DriverChain([ new YamlDriver($fileLocator, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator), new XmlDriver($fileLocator, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator), new AnnotationDriver($annotationReader, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator), ]); + } else { + $driver = new AnnotationDriver($annotationReader, $this->propertyNamingStrategy, $this->typeParser); } - return new AnnotationDriver($annotationReader, $this->propertyNamingStrategy, $this->typeParser); + if (PHP_VERSION_ID >= 70400) { + $driver = new TypedPropertiesDriver($driver, $this->typeParser); + } + + return $driver; } } diff --git a/src/Construction/DoctrineObjectConstructor.php b/src/Construction/DoctrineObjectConstructor.php index 31d5f8703..340c0a1d5 100644 --- a/src/Construction/DoctrineObjectConstructor.php +++ b/src/Construction/DoctrineObjectConstructor.php @@ -4,11 +4,13 @@ namespace JMS\Serializer\Construction; -use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\Persistence\ManagerRegistry; use JMS\Serializer\DeserializationContext; use JMS\Serializer\Exception\InvalidArgumentException; use JMS\Serializer\Exception\ObjectConstructionException; +use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy; use JMS\Serializer\Metadata\ClassMetadata; +use JMS\Serializer\Metadata\PropertyMetadata; use JMS\Serializer\Visitor\DeserializationVisitorInterface; /** @@ -34,15 +36,25 @@ final class DoctrineObjectConstructor implements ObjectConstructorInterface */ private $fallbackConstructor; + /** + * @var ExpressionLanguageExclusionStrategy|null + */ + private $expressionLanguageExclusionStrategy; + /** * @param ManagerRegistry $managerRegistry Manager registry * @param ObjectConstructorInterface $fallbackConstructor Fallback object constructor */ - public function __construct(ManagerRegistry $managerRegistry, ObjectConstructorInterface $fallbackConstructor, string $fallbackStrategy = self::ON_MISSING_NULL) - { + public function __construct( + ManagerRegistry $managerRegistry, + ObjectConstructorInterface $fallbackConstructor, + string $fallbackStrategy = self::ON_MISSING_NULL, + ?ExpressionLanguageExclusionStrategy $expressionLanguageExclusionStrategy = null + ) { $this->managerRegistry = $managerRegistry; $this->fallbackConstructor = $fallbackConstructor; $this->fallbackStrategy = $fallbackStrategy; + $this->expressionLanguageExclusionStrategy = $expressionLanguageExclusionStrategy; } /** @@ -67,7 +79,7 @@ public function construct(DeserializationVisitorInterface $visitor, ClassMetadat } // Managed entity, check for proxy load - if (!\is_array($data)) { + if (!\is_array($data) && !(is_object($data) && 'SimpleXMLElement' === get_class($data))) { // Single identifier, load proxy return $objectManager->getReference($metadata->name, $data); } @@ -77,16 +89,23 @@ public function construct(DeserializationVisitorInterface $visitor, ClassMetadat $identifierList = []; foreach ($classMetadata->getIdentifierFieldNames() as $name) { - if (isset($metadata->propertyMetadata[$name])) { - $dataName = $metadata->propertyMetadata[$name]->serializedName; - } else { - $dataName = $name; + // Avoid calling objectManager->find if some identification properties are excluded + if (!isset($metadata->propertyMetadata[$name])) { + return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context); + } + + $propertyMetadata = $metadata->propertyMetadata[$name]; + + // Avoid calling objectManager->find if some identification properties are excluded by some exclusion strategy + if ($this->isIdentifierFieldExcluded($propertyMetadata, $context)) { + return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context); } - if (!array_key_exists($dataName, $data)) { + if (!array_key_exists($propertyMetadata->serializedName, $data)) { return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context); } - $identifierList[$name] = $data[$dataName]; + + $identifierList[$name] = $data[$propertyMetadata->serializedName]; } if (empty($identifierList)) { @@ -102,10 +121,13 @@ public function construct(DeserializationVisitorInterface $visitor, ClassMetadat switch ($this->fallbackStrategy) { case self::ON_MISSING_NULL: return null; + case self::ON_MISSING_EXCEPTION: throw new ObjectConstructionException(sprintf('Entity %s can not be found', $metadata->name)); + case self::ON_MISSING_FALLBACK: return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context); + default: throw new InvalidArgumentException('The provided fallback strategy for the object constructor is not valid'); } @@ -115,4 +137,14 @@ public function construct(DeserializationVisitorInterface $visitor, ClassMetadat return $object; } + + private function isIdentifierFieldExcluded(PropertyMetadata $propertyMetadata, DeserializationContext $context): bool + { + $exclusionStrategy = $context->getExclusionStrategy(); + if (null !== $exclusionStrategy && $exclusionStrategy->shouldSkipProperty($propertyMetadata, $context)) { + return true; + } + + return null !== $this->expressionLanguageExclusionStrategy && $this->expressionLanguageExclusionStrategy->shouldSkipProperty($propertyMetadata, $context); + } } diff --git a/src/Context.php b/src/Context.php index 379bd6ef7..36adc2e69 100644 --- a/src/Context.php +++ b/src/Context.php @@ -121,6 +121,8 @@ public function hasAttribute(string $key): bool /** * @param mixed $value + * + * @return $this */ public function setAttribute(string $key, $value): self { @@ -130,7 +132,7 @@ public function setAttribute(string $key, $value): self return $this; } - private function assertMutable(): void + final protected function assertMutable(): void { if (!$this->initialized) { return; @@ -139,17 +141,22 @@ private function assertMutable(): void throw new LogicException('This context was already initialized and is immutable; you cannot modify it anymore.'); } + /** + * @return $this + */ public function addExclusionStrategy(ExclusionStrategyInterface $strategy): self { $this->assertMutable(); if (null === $this->exclusionStrategy) { $this->exclusionStrategy = $strategy; + return $this; } if ($this->exclusionStrategy instanceof DisjunctExclusionStrategy) { $this->exclusionStrategy->addStrategy($strategy); + return $this; } @@ -161,6 +168,9 @@ public function addExclusionStrategy(ExclusionStrategyInterface $strategy): self return $this; } + /** + * @return $this + */ public function setVersion(string $version): self { $this->attributes['version'] = $version; @@ -170,6 +180,8 @@ public function setVersion(string $version): self /** * @param array|string $groups + * + * @return $this */ public function setGroups($groups): self { @@ -182,6 +194,9 @@ public function setGroups($groups): self return $this; } + /** + * @return $this + */ public function enableMaxDepthChecks(): self { $this->attributes['max_depth_checks'] = true; diff --git a/src/ContextFactory/CallableDeserializationContextFactory.php b/src/ContextFactory/CallableDeserializationContextFactory.php index f6bd73e1c..d64ce6084 100644 --- a/src/ContextFactory/CallableDeserializationContextFactory.php +++ b/src/ContextFactory/CallableDeserializationContextFactory.php @@ -12,9 +12,6 @@ final class CallableDeserializationContextFactory extends CallableContextFactory implements DeserializationContextFactoryInterface { - /** - * {@InheritDoc} - */ public function createDeserializationContext(): DeserializationContext { return $this->createContext(); diff --git a/src/ContextFactory/CallableSerializationContextFactory.php b/src/ContextFactory/CallableSerializationContextFactory.php index e399fd059..ccc179a18 100644 --- a/src/ContextFactory/CallableSerializationContextFactory.php +++ b/src/ContextFactory/CallableSerializationContextFactory.php @@ -12,9 +12,6 @@ final class CallableSerializationContextFactory extends CallableContextFactory implements SerializationContextFactoryInterface { - /** - * {@InheritDoc} - */ public function createSerializationContext(): SerializationContext { return $this->createContext(); diff --git a/src/ContextFactory/DefaultDeserializationContextFactory.php b/src/ContextFactory/DefaultDeserializationContextFactory.php index 03313faea..1579aa3c5 100644 --- a/src/ContextFactory/DefaultDeserializationContextFactory.php +++ b/src/ContextFactory/DefaultDeserializationContextFactory.php @@ -11,9 +11,6 @@ */ final class DefaultDeserializationContextFactory implements DeserializationContextFactoryInterface { - /** - * {@InheritDoc} - */ public function createDeserializationContext(): DeserializationContext { return new DeserializationContext(); diff --git a/src/ContextFactory/DefaultSerializationContextFactory.php b/src/ContextFactory/DefaultSerializationContextFactory.php index d1d8ce799..95ed2ee69 100644 --- a/src/ContextFactory/DefaultSerializationContextFactory.php +++ b/src/ContextFactory/DefaultSerializationContextFactory.php @@ -11,9 +11,6 @@ */ final class DefaultSerializationContextFactory implements SerializationContextFactoryInterface { - /** - * {@InheritDoc} - */ public function createSerializationContext(): SerializationContext { return new SerializationContext(); diff --git a/src/EventDispatcher/EventDispatcher.php b/src/EventDispatcher/EventDispatcher.php index 3c6c1e671..2433eda40 100644 --- a/src/EventDispatcher/EventDispatcher.php +++ b/src/EventDispatcher/EventDispatcher.php @@ -90,7 +90,7 @@ public function dispatch(string $eventName, string $class, string $format, Event $object = $event instanceof ObjectEvent ? $event->getObject() : null; $realClass = is_object($object) ? get_class($object) : ''; - $objectClass = $realClass !== $class ? ($realClass . $class) : $class; + $objectClass = $realClass !== $class ? $realClass . $class : $class; if (!isset($this->classListeners[$eventName][$objectClass][$format])) { $this->classListeners[$eventName][$objectClass][$format] = $this->initializeListeners($eventName, $class, $format); @@ -119,6 +119,7 @@ protected function initializeListeners(string $eventName, string $loweredClass, if (null !== $listener[1] && $loweredClass !== $listener[1]) { continue; } + if (null !== $listener[2] && $format !== $listener[2]) { continue; } diff --git a/src/EventDispatcher/Subscriber/DoctrineProxySubscriber.php b/src/EventDispatcher/Subscriber/DoctrineProxySubscriber.php index 3e343a4fc..b39ded887 100644 --- a/src/EventDispatcher/Subscriber/DoctrineProxySubscriber.php +++ b/src/EventDispatcher/Subscriber/DoctrineProxySubscriber.php @@ -4,31 +4,32 @@ namespace JMS\Serializer\EventDispatcher\Subscriber; -use Doctrine\Common\Persistence\Proxy; +use Doctrine\Common\Persistence\Proxy as LegacyProxy; use Doctrine\ODM\MongoDB\PersistentCollection as MongoDBPersistentCollection; use Doctrine\ODM\PHPCR\PersistentCollection as PHPCRPersistentCollection; use Doctrine\ORM\PersistentCollection; -use Doctrine\ORM\Proxy\Proxy as ORMProxy; +use Doctrine\Persistence\Proxy; use JMS\Serializer\EventDispatcher\EventDispatcherInterface; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\PreSerializeEvent; +use ProxyManager\Proxy\LazyLoadingInterface; final class DoctrineProxySubscriber implements EventSubscriberInterface { /** * @var bool */ - private $skipVirtualTypeInit = true; + private $skipVirtualTypeInit; /** * @var bool */ - private $initializeExcluded = false; + private $initializeExcluded; public function __construct(bool $skipVirtualTypeInit = true, bool $initializeExcluded = false) { - $this->skipVirtualTypeInit = (bool) $skipVirtualTypeInit; - $this->initializeExcluded = (bool) $initializeExcluded; + $this->skipVirtualTypeInit = $skipVirtualTypeInit; + $this->initializeExcluded = $initializeExcluded; } public function onPreSerialize(PreSerializeEvent $event): void @@ -41,7 +42,8 @@ public function onPreSerialize(PreSerializeEvent $event): void // so it must be loaded if its a real class. $virtualType = !class_exists($type['name'], false); - if ($object instanceof PersistentCollection + if ( + $object instanceof PersistentCollection || $object instanceof MongoDBPersistentCollection || $object instanceof PHPCRPersistentCollection ) { @@ -52,8 +54,9 @@ public function onPreSerialize(PreSerializeEvent $event): void return; } - if (($this->skipVirtualTypeInit && $virtualType) || - (!$object instanceof Proxy && !$object instanceof ORMProxy) + if ( + ($this->skipVirtualTypeInit && $virtualType) || + (!$object instanceof Proxy && !$object instanceof LazyLoadingInterface) ) { return; } @@ -68,7 +71,11 @@ public function onPreSerialize(PreSerializeEvent $event): void } } - $object->__load(); + if ($object instanceof LazyLoadingInterface) { + $object->initializeProxy(); + } else { + $object->__load(); + } if (!$virtualType) { $event->setType(get_parent_class($object), $type['params']); @@ -107,10 +114,13 @@ public static function getSubscribedEvents() { return [ ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerializeTypedProxy', 'interface' => Proxy::class], + ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerializeTypedProxy', 'interface' => LegacyProxy::class], ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => PersistentCollection::class], ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => MongoDBPersistentCollection::class], ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => PHPCRPersistentCollection::class], ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => Proxy::class], + ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => LegacyProxy::class], + ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => LazyLoadingInterface::class], ]; } } diff --git a/src/Exception/SkipHandlerException.php b/src/Exception/SkipHandlerException.php new file mode 100755 index 000000000..83a13a5cb --- /dev/null +++ b/src/Exception/SkipHandlerException.php @@ -0,0 +1,13 @@ +isTooDeep($context); } - /** - * {@inheritDoc} - */ public function shouldSkipProperty(PropertyMetadata $property, Context $context): bool { return $this->isTooDeep($context); diff --git a/src/Exclusion/DisjunctExclusionStrategy.php b/src/Exclusion/DisjunctExclusionStrategy.php index 78e061c0e..7942798bc 100644 --- a/src/Exclusion/DisjunctExclusionStrategy.php +++ b/src/Exclusion/DisjunctExclusionStrategy.php @@ -20,7 +20,7 @@ final class DisjunctExclusionStrategy implements ExclusionStrategyInterface /** * @var ExclusionStrategyInterface[] */ - private $delegates = []; + private $delegates; /** * @param ExclusionStrategyInterface[] $delegates @@ -41,7 +41,7 @@ public function addStrategy(ExclusionStrategyInterface $strategy): void public function shouldSkipClass(ClassMetadata $metadata, Context $context): bool { foreach ($this->delegates as $delegate) { - /** @var $delegate ExclusionStrategyInterface */ + \assert($delegate instanceof ExclusionStrategyInterface); if ($delegate->shouldSkipClass($metadata, $context)) { return true; } @@ -56,7 +56,7 @@ public function shouldSkipClass(ClassMetadata $metadata, Context $context): bool public function shouldSkipProperty(PropertyMetadata $property, Context $context): bool { foreach ($this->delegates as $delegate) { - /** @var $delegate ExclusionStrategyInterface */ + \assert($delegate instanceof ExclusionStrategyInterface); if ($delegate->shouldSkipProperty($property, $context)) { return true; } diff --git a/src/Exclusion/ExpressionLanguageExclusionStrategy.php b/src/Exclusion/ExpressionLanguageExclusionStrategy.php index d733c6fcc..d15d5af6d 100644 --- a/src/Exclusion/ExpressionLanguageExclusionStrategy.php +++ b/src/Exclusion/ExpressionLanguageExclusionStrategy.php @@ -8,6 +8,7 @@ use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface; use JMS\Serializer\Expression\Expression; use JMS\Serializer\Expression\ExpressionEvaluatorInterface; +use JMS\Serializer\Metadata\ClassMetadata; use JMS\Serializer\Metadata\PropertyMetadata; use JMS\Serializer\SerializationContext; @@ -31,9 +32,29 @@ public function __construct(ExpressionEvaluatorInterface $expressionEvaluator) $this->expressionEvaluator = $expressionEvaluator; } - /** - * {@inheritDoc} - */ + public function shouldSkipClass(ClassMetadata $class, Context $navigatorContext): bool + { + if (null === $class->excludeIf) { + return false; + } + + $variables = [ + 'context' => $navigatorContext, + 'class_metadata' => $class, + ]; + if ($navigatorContext instanceof SerializationContext) { + $variables['object'] = $navigatorContext->getObject(); + } else { + $variables['object'] = null; + } + + if (($class->excludeIf instanceof Expression) && ($this->expressionEvaluator instanceof CompilableExpressionEvaluatorInterface)) { + return $this->expressionEvaluator->evaluateParsed($class->excludeIf, $variables); + } + + return $this->expressionEvaluator->evaluate($class->excludeIf, $variables); + } + public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext): bool { if (null === $property->excludeIf) { diff --git a/src/Exclusion/GroupsExclusionStrategy.php b/src/Exclusion/GroupsExclusionStrategy.php index 66762826f..8390f6296 100644 --- a/src/Exclusion/GroupsExclusionStrategy.php +++ b/src/Exclusion/GroupsExclusionStrategy.php @@ -44,17 +44,11 @@ public function __construct(array $groups) } } - /** - * {@inheritDoc} - */ public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext): bool { return false; } - /** - * {@inheritDoc} - */ public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext): bool { if ($this->nestedGroups) { @@ -75,6 +69,7 @@ public function shouldSkipProperty(PropertyMetadata $property, Context $navigato return false; } } + return true; } } @@ -103,13 +98,16 @@ public function getGroupsFor(Context $navigatorContext): array if ($index > 0) { $groups = [self::DEFAULT_GROUP]; } + break; } + $groups = $groups[$path]; if (!array_filter($groups, 'is_string')) { $groups += [self::DEFAULT_GROUP]; } } + return $groups; } } diff --git a/src/Exclusion/VersionExclusionStrategy.php b/src/Exclusion/VersionExclusionStrategy.php index 99ef117ca..be33ce8b2 100644 --- a/src/Exclusion/VersionExclusionStrategy.php +++ b/src/Exclusion/VersionExclusionStrategy.php @@ -20,27 +20,17 @@ public function __construct(string $version) $this->version = $version; } - /** - * {@inheritDoc} - */ public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext): bool { return false; } - /** - * {@inheritDoc} - */ public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext): bool { if ((null !== $version = $property->sinceVersion) && version_compare($this->version, $version, '<')) { return true; } - if ((null !== $version = $property->untilVersion) && version_compare($this->version, $version, '>')) { - return true; - } - - return false; + return (null !== $version = $property->untilVersion) && version_compare($this->version, $version, '>'); } } diff --git a/src/Expression/ExpressionEvaluator.php b/src/Expression/ExpressionEvaluator.php index 9dd511f2a..95d15f668 100644 --- a/src/Expression/ExpressionEvaluator.php +++ b/src/Expression/ExpressionEvaluator.php @@ -19,7 +19,7 @@ class ExpressionEvaluator implements CompilableExpressionEvaluatorInterface, Exp /** * @var array */ - private $context = []; + private $context; public function __construct(ExpressionLanguage $expressionLanguage, array $context = []) { diff --git a/src/GraphNavigator/DeserializationGraphNavigator.php b/src/GraphNavigator/DeserializationGraphNavigator.php index 2cd1c5408..00889d1da 100644 --- a/src/GraphNavigator/DeserializationGraphNavigator.php +++ b/src/GraphNavigator/DeserializationGraphNavigator.php @@ -15,9 +15,9 @@ use JMS\Serializer\Exception\LogicException; use JMS\Serializer\Exception\NotAcceptableException; use JMS\Serializer\Exception\RuntimeException; +use JMS\Serializer\Exception\SkipHandlerException; use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy; use JMS\Serializer\Expression\ExpressionEvaluatorInterface; -use JMS\Serializer\Functions; use JMS\Serializer\GraphNavigator; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\Handler\HandlerRegistryInterface; @@ -108,6 +108,7 @@ public function accept($data, ?array $type = null) if (null === $type) { throw new RuntimeException('The type must be given for all properties when deserializing.'); } + // Sometimes data can convey null but is not of a null type. // Visitors can have the power to add this custom null evaluation if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) { @@ -134,7 +135,7 @@ public function accept($data, ?array $type = null) return $this->visitor->visitDouble($data, $type); case 'iterable': - return $this->visitor->visitArray(Functions::iterableToArray($data), $type); + return $this->visitor->visitArray($data, $type); case 'array': return $this->visitor->visitArray($data, $type); @@ -157,14 +158,18 @@ public function accept($data, ?array $type = null) // before loading metadata because the type name might not be a class, but // could also simply be an artifical type. if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_DESERIALIZATION, $type['name'], $this->format)) { - $rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context); - $this->context->decreaseDepth(); + try { + $rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context); + $this->context->decreaseDepth(); - return $rs; + return $rs; + } catch (SkipHandlerException $e) { + // Skip handler, fallback to default behavior + } } - /** @var ClassMetadata $metadata */ $metadata = $this->metadataFactory->getMetadataForClass($type['name']); + \assert($metadata instanceof ClassMetadata); if ($metadata->usingExpression && !$this->expressionExclusionStrategy) { throw new ExpressionLanguageRequiredException(sprintf('To use conditional exclude/expose in %s you must configure the expression language.', $metadata->name)); @@ -184,6 +189,13 @@ public function accept($data, ?array $type = null) $object = $this->objectConstructor->construct($this->visitor, $metadata, $data, $type, $this->context); + if (null === $object) { + $this->context->popClassMetadata(); + $this->context->decreaseDepth(); + + return $this->visitor->visitNull($data, $type); + } + $this->visitor->startVisitingObject($metadata, $object, $type); foreach ($metadata->propertyMetadata as $propertyMetadata) { if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) { @@ -204,6 +216,7 @@ public function accept($data, ?array $type = null) $this->accessor->setValue($object, $v, $propertyMetadata, $this->context); } catch (NotAcceptableException $e) { } + $this->context->popPropertyMetadata(); } diff --git a/src/GraphNavigator/SerializationGraphNavigator.php b/src/GraphNavigator/SerializationGraphNavigator.php index dfa25b17e..31ecd01ba 100644 --- a/src/GraphNavigator/SerializationGraphNavigator.php +++ b/src/GraphNavigator/SerializationGraphNavigator.php @@ -15,6 +15,7 @@ use JMS\Serializer\Exception\ExpressionLanguageRequiredException; use JMS\Serializer\Exception\NotAcceptableException; use JMS\Serializer\Exception\RuntimeException; +use JMS\Serializer\Exception\SkipHandlerException; use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy; use JMS\Serializer\Expression\ExpressionEvaluatorInterface; use JMS\Serializer\Functions; @@ -124,6 +125,7 @@ public function accept($data, ?array $type = null) // guarantee correct handling of null values, and not have any internal auto-casting behavior. $type = ['name' => 'NULL', 'params' => []]; } + // Sometimes data can convey null but is not of a null type. // Visitors can have the power to add this custom null evaluation if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) { @@ -132,9 +134,10 @@ public function accept($data, ?array $type = null) switch ($type['name']) { case 'NULL': - if (!$this->shouldSerializeNull) { + if (!$this->shouldSerializeNull && !$this->isRootNullAllowed()) { throw new NotAcceptableException(); } + return $this->visitor->visitNull($data, $type); case 'string': @@ -171,6 +174,7 @@ public function accept($data, ?array $type = null) if ($this->context->isVisiting($data)) { throw new CircularReferenceDetectedException(); } + $this->context->startVisiting($data); } @@ -178,7 +182,7 @@ public function accept($data, ?array $type = null) // metadata for the actual type of the object, not the base class. if (class_exists($type['name'], false) || interface_exists($type['name'], false)) { if (is_subclass_of($data, $type['name'], false)) { - $type = ['name' => \get_class($data), 'params' => []]; + $type = ['name' => \get_class($data), 'params' => $type['params'] ?? []]; } } @@ -195,15 +199,20 @@ public function accept($data, ?array $type = null) if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) { try { $rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context); - } finally { $this->context->stopVisiting($data); - } - return $rs; + return $rs; + } catch (SkipHandlerException $e) { + // Skip handler, fallback to default behavior + } catch (NotAcceptableException $e) { + $this->context->stopVisiting($data); + + throw $e; + } } - /** @var ClassMetadata $metadata */ $metadata = $this->metadataFactory->getMetadataForClass($type['name']); + \assert($metadata instanceof ClassMetadata); if ($metadata->usingExpression && null === $this->expressionExclusionStrategy) { throw new ExpressionLanguageRequiredException(sprintf('To use conditional exclude/expose in %s you must configure the expression language.', $metadata->name)); @@ -215,6 +224,12 @@ public function accept($data, ?array $type = null) throw new ExcludedClassException(); } + if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipClass($metadata, $this->context)) { + $this->context->stopVisiting($data); + + throw new ExcludedClassException(); + } + $this->context->pushClassMetadata($metadata); foreach ($metadata->preSerializeMethods as $method) { @@ -248,6 +263,11 @@ public function accept($data, ?array $type = null) } } + private function isRootNullAllowed(): bool + { + return $this->context->hasAttribute('allows_root_null') && $this->context->getAttribute('allows_root_null') && 0 === $this->context->getVisitingSet()->count(); + } + private function afterVisitingObject(ClassMetadata $metadata, object $object, array $type): void { $this->context->stopVisiting($object); diff --git a/src/Handler/ArrayCollectionHandler.php b/src/Handler/ArrayCollectionHandler.php index f0bd6b36d..ae839bd7f 100644 --- a/src/Handler/ArrayCollectionHandler.php +++ b/src/Handler/ArrayCollectionHandler.php @@ -17,7 +17,7 @@ final class ArrayCollectionHandler implements SubscribingHandlerInterface /** * @var bool */ - private $initializeExcluded = true; + private $initializeExcluded; public function __construct(bool $initializeExcluded = true) { @@ -78,9 +78,11 @@ public function serializeCollection(SerializationVisitorInterface $visitor, Coll return $visitor->visitArray([], $type, $context); } } + $result = $visitor->visitArray($collection->toArray(), $type); $context->startVisiting($collection); + return $result; } diff --git a/src/Handler/ConstraintViolationHandler.php b/src/Handler/ConstraintViolationHandler.php index 3efb5fd1c..d46291eb9 100644 --- a/src/Handler/ConstraintViolationHandler.php +++ b/src/Handler/ConstraintViolationHandler.php @@ -5,8 +5,8 @@ namespace JMS\Serializer\Handler; use JMS\Serializer\GraphNavigatorInterface; -use JMS\Serializer\JsonSerializationVisitor; use JMS\Serializer\SerializationContext; +use JMS\Serializer\Visitor\SerializationVisitorInterface; use JMS\Serializer\XmlSerializationVisitor; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; @@ -51,7 +51,7 @@ public function serializeListToXml(XmlSerializationVisitor $visitor, ConstraintV /** * @return array|\ArrayObject */ - public function serializeListToJson(JsonSerializationVisitor $visitor, ConstraintViolationList $list, array $type, SerializationContext $context) + public function serializeListToJson(SerializationVisitorInterface $visitor, ConstraintViolationList $list, array $type, SerializationContext $context) { return $visitor->visitArray(iterator_to_array($list), $type); } @@ -73,7 +73,7 @@ public function serializeViolationToXml(XmlSerializationVisitor $visitor, Constr $messageNode->appendChild($visitor->getDocument()->createCDATASection($violation->getMessage())); } - public function serializeViolationToJson(JsonSerializationVisitor $visitor, ConstraintViolation $violation, ?array $type = null): array + public function serializeViolationToJson(SerializationVisitorInterface $visitor, ConstraintViolation $violation, ?array $type = null): array { return [ 'property_path' => $violation->getPropertyPath(), diff --git a/src/Handler/DateHandler.php b/src/Handler/DateHandler.php index cf95a8631..4f0e74afe 100644 --- a/src/Handler/DateHandler.php +++ b/src/Handler/DateHandler.php @@ -6,10 +6,9 @@ use JMS\Serializer\Exception\RuntimeException; use JMS\Serializer\GraphNavigatorInterface; -use JMS\Serializer\JsonDeserializationVisitor; use JMS\Serializer\SerializationContext; +use JMS\Serializer\Visitor\DeserializationVisitorInterface; use JMS\Serializer\Visitor\SerializationVisitorInterface; -use JMS\Serializer\XmlDeserializationVisitor; use JMS\Serializer\XmlSerializationVisitor; final class DateHandler implements SubscribingHandlerInterface @@ -35,19 +34,15 @@ final class DateHandler implements SubscribingHandlerInterface public static function getSubscribingMethods() { $methods = []; - $deserializationTypes = ['DateTime', 'DateTimeImmutable', 'DateInterval']; - $serialisationTypes = ['DateTime', 'DateTimeImmutable', 'DateInterval']; + $types = ['DateTime', 'DateTimeImmutable', 'DateInterval']; foreach (['json', 'xml'] as $format) { - foreach ($deserializationTypes as $type) { + foreach ($types as $type) { $methods[] = [ 'type' => $type, 'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION, 'format' => $format, ]; - } - - foreach ($serialisationTypes as $type) { $methods[] = [ 'type' => $type, 'format' => $format, @@ -55,6 +50,13 @@ public static function getSubscribingMethods() 'method' => 'serialize' . $type, ]; } + + $methods[] = [ + 'type' => 'DateTimeInterface', + 'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION, + 'format' => $format, + 'method' => 'deserializeDateTimeFrom' . ucfirst($format), + ]; } return $methods; @@ -134,6 +136,7 @@ public function serializeDateInterval(SerializationVisitorInterface $visitor, \D private function isDataXmlNull($data): bool { $attributes = $data->attributes('xsi', true); + return isset($attributes['nil'][0]) && 'true' === (string) $attributes['nil'][0]; } @@ -141,7 +144,7 @@ private function isDataXmlNull($data): bool * @param mixed $data * @param array $type */ - public function deserializeDateTimeFromXml(XmlDeserializationVisitor $visitor, $data, array $type): ?\DateTimeInterface + public function deserializeDateTimeFromXml(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateTimeInterface { if ($this->isDataXmlNull($data)) { return null; @@ -154,7 +157,7 @@ public function deserializeDateTimeFromXml(XmlDeserializationVisitor $visitor, $ * @param mixed $data * @param array $type */ - public function deserializeDateTimeImmutableFromXml(XmlDeserializationVisitor $visitor, $data, array $type): ?\DateTimeInterface + public function deserializeDateTimeImmutableFromXml(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateTimeInterface { if ($this->isDataXmlNull($data)) { return null; @@ -167,7 +170,7 @@ public function deserializeDateTimeImmutableFromXml(XmlDeserializationVisitor $v * @param mixed $data * @param array $type */ - public function deserializeDateIntervalFromXml(XmlDeserializationVisitor $visitor, $data, array $type): ?\DateInterval + public function deserializeDateIntervalFromXml(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateInterval { if ($this->isDataXmlNull($data)) { return null; @@ -180,7 +183,7 @@ public function deserializeDateIntervalFromXml(XmlDeserializationVisitor $visito * @param mixed $data * @param array $type */ - public function deserializeDateTimeFromJson(JsonDeserializationVisitor $visitor, $data, array $type): ?\DateTimeInterface + public function deserializeDateTimeFromJson(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateTimeInterface { if (null === $data) { return null; @@ -193,7 +196,7 @@ public function deserializeDateTimeFromJson(JsonDeserializationVisitor $visitor, * @param mixed $data * @param array $type */ - public function deserializeDateTimeImmutableFromJson(JsonDeserializationVisitor $visitor, $data, array $type): ?\DateTimeInterface + public function deserializeDateTimeImmutableFromJson(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateTimeInterface { if (null === $data) { return null; @@ -206,7 +209,7 @@ public function deserializeDateTimeImmutableFromJson(JsonDeserializationVisitor * @param mixed $data * @param array $type */ - public function deserializeDateIntervalFromJson(JsonDeserializationVisitor $visitor, $data, array $type): ?\DateInterval + public function deserializeDateIntervalFromJson(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateInterval { if (null === $data) { return null; @@ -222,32 +225,48 @@ public function deserializeDateIntervalFromJson(JsonDeserializationVisitor $visi private function parseDateTime($data, array $type, bool $immutable = false): \DateTimeInterface { $timezone = !empty($type['params'][1]) ? new \DateTimeZone($type['params'][1]) : $this->defaultTimezone; - $format = $this->getDeserializationFormat($type); + $formats = $this->getDeserializationFormats($type); + + $formatTried = []; + foreach ($formats as $format) { + if ($immutable) { + $datetime = \DateTimeImmutable::createFromFormat($format, (string) $data, $timezone); + } else { + $datetime = \DateTime::createFromFormat($format, (string) $data, $timezone); + } - if ($immutable) { - $datetime = \DateTimeImmutable::createFromFormat($format, (string) $data, $timezone); - } else { - $datetime = \DateTime::createFromFormat($format, (string) $data, $timezone); - } + if (false !== $datetime) { + if ('U' === $format) { + $datetime = $datetime->setTimezone($timezone); + } - if (false === $datetime) { - throw new RuntimeException(sprintf('Invalid datetime "%s", expected format %s.', $data, $format)); - } + return $datetime; + } - if ('U' === $format) { - $datetime = $datetime->setTimezone($timezone); + $formatTried[] = $format; } - return $datetime; + throw new RuntimeException(sprintf( + 'Invalid datetime "%s", expected one of the format %s.', + $data, + '"' . implode('", "', $formatTried) . '"' + )); } private function parseDateInterval(string $data): \DateInterval { $dateInterval = null; try { + $f = 0.0; + if (preg_match('~\.\d+~', $data, $match)) { + $data = str_replace($match[0], '', $data); + $f = (float) $match[0]; + } + $dateInterval = new \DateInterval($data); + $dateInterval->f = $f; } catch (\Throwable $e) { - throw new RuntimeException(sprintf('Invalid dateinterval "%s", expected ISO 8601 format', $data), null, $e); + throw new RuntimeException(sprintf('Invalid dateinterval "%s", expected ISO 8601 format', $data), 0, $e); } return $dateInterval; @@ -256,15 +275,13 @@ private function parseDateInterval(string $data): \DateInterval /** * @param array $type */ - private function getDeserializationFormat(array $type): string + private function getDeserializationFormats(array $type): array { if (isset($type['params'][2])) { - return $type['params'][2]; - } - if (isset($type['params'][0])) { - return $type['params'][0]; + return is_array($type['params'][2]) ? $type['params'][2] : [$type['params'][2]]; } - return $this->defaultFormat; + + return [$this->getFormat($type)]; } /** diff --git a/src/Handler/FormErrorHandler.php b/src/Handler/FormErrorHandler.php index e3e76a8f1..b5101960b 100644 --- a/src/Handler/FormErrorHandler.php +++ b/src/Handler/FormErrorHandler.php @@ -5,12 +5,12 @@ namespace JMS\Serializer\Handler; use JMS\Serializer\GraphNavigatorInterface; -use JMS\Serializer\JsonSerializationVisitor; use JMS\Serializer\Visitor\SerializationVisitorInterface; use JMS\Serializer\XmlSerializationVisitor; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormError; use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface as TranslatorContract; final class FormErrorHandler implements SubscribingHandlerInterface { @@ -24,7 +24,6 @@ final class FormErrorHandler implements SubscribingHandlerInterface */ private $translationDomain; - /** * {@inheritdoc} */ @@ -47,8 +46,17 @@ public static function getSubscribingMethods() return $methods; } - public function __construct(?TranslatorInterface $translator = null, string $translationDomain = 'validators') + public function __construct(?object $translator = null, string $translationDomain = 'validators') { + if (null !== $translator && (!$translator instanceof TranslatorInterface && !$translator instanceof TranslatorContract)) { + throw new \InvalidArgumentException(sprintf( + 'The first argument passed to %s must be instance of %s or %s, %s given', + self::class, + TranslatorInterface::class, + TranslatorContract::class + )); + } + $this->translator = $translator; $this->translationDomain = $translationDomain; } @@ -83,7 +91,7 @@ public function serializeFormToXml(XmlSerializationVisitor $visitor, Form $form, /** * @param array $type */ - public function serializeFormToJson(JsonSerializationVisitor $visitor, Form $form, array $type): \ArrayObject + public function serializeFormToJson(SerializationVisitorInterface $visitor, Form $form, array $type): \ArrayObject { return $this->convertFormToArray($visitor, $form); } @@ -99,7 +107,7 @@ public function serializeFormErrorToXml(XmlSerializationVisitor $visitor, FormEr /** * @param array $type */ - public function serializeFormErrorToJson(JsonSerializationVisitor $visitor, FormError $formError, array $type): string + public function serializeFormErrorToJson(SerializationVisitorInterface $visitor, FormError $formError, array $type): string { return $this->getErrorMessage($formError); } @@ -111,7 +119,11 @@ private function getErrorMessage(FormError $error): ?string } if (null !== $error->getMessagePluralization()) { - return $this->translator->transChoice($error->getMessageTemplate(), $error->getMessagePluralization(), $error->getMessageParameters(), $this->translationDomain); + if ($this->translator instanceof TranslatorContract) { + return $this->translator->trans($error->getMessageTemplate(), ['%count%' => $error->getMessagePluralization()] + $error->getMessageParameters(), $this->translationDomain); + } else { + return $this->translator->transChoice($error->getMessageTemplate(), $error->getMessagePluralization(), $error->getMessageParameters(), $this->translationDomain); + } } return $this->translator->trans($error->getMessageTemplate(), $error->getMessageParameters(), $this->translationDomain); diff --git a/src/Handler/IteratorHandler.php b/src/Handler/IteratorHandler.php index b0971f59d..e8e02b0a5 100644 --- a/src/Handler/IteratorHandler.php +++ b/src/Handler/IteratorHandler.php @@ -143,7 +143,6 @@ public function deserializeIterable( return $return; } - /** * @param mixed $data */ diff --git a/src/JsonDeserializationVisitor.php b/src/JsonDeserializationVisitor.php index d78a31efb..e4f8d8637 100644 --- a/src/JsonDeserializationVisitor.php +++ b/src/JsonDeserializationVisitor.php @@ -16,12 +16,12 @@ final class JsonDeserializationVisitor extends AbstractVisitor implements Deseri /** * @var int */ - private $options = 0; + private $options; /** * @var int */ - private $depth = 512; + private $depth; /** * @var \SplStack @@ -166,6 +166,7 @@ public function visitProperty(PropertyMetadata $metadata, $data) if (!$metadata->type) { throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name); } + return $this->navigator->accept($data, $metadata->type); } diff --git a/src/JsonSerializationVisitor.php b/src/JsonSerializationVisitor.php index 695f4fa24..fbee58249 100644 --- a/src/JsonSerializationVisitor.php +++ b/src/JsonSerializationVisitor.php @@ -15,12 +15,12 @@ final class JsonSerializationVisitor extends AbstractVisitor implements Serializ /** * @var int */ - private $options = JSON_PRESERVE_ZERO_FRACTION; + private $options; /** * @var array */ - private $dataStack = []; + private $dataStack; /** * @var \ArrayObject */ @@ -103,6 +103,7 @@ public function visitArray(array $data, array $type) } \array_pop($this->dataStack); + return $rs; } @@ -156,8 +157,6 @@ public function visitProperty(PropertyMetadata $metadata, $v): void } /** - * @deprecated Will be removed in 3.0 - * * Checks if some data key exists. */ public function hasData(string $key): bool diff --git a/src/Metadata/ClassMetadata.php b/src/Metadata/ClassMetadata.php index 18ae38ae2..1b6bab2f7 100644 --- a/src/Metadata/ClassMetadata.php +++ b/src/Metadata/ClassMetadata.php @@ -5,6 +5,7 @@ namespace JMS\Serializer\Metadata; use JMS\Serializer\Exception\InvalidMetadataException; +use JMS\Serializer\Expression\Expression; use JMS\Serializer\Ordering\AlphabeticalPropertyOrderingStrategy; use JMS\Serializer\Ordering\CustomPropertyOrderingStrategy; use JMS\Serializer\Ordering\IdenticalPropertyOrderingStrategy; @@ -126,6 +127,11 @@ class ClassMetadata extends MergeableClassMetadata */ public $xmlDiscriminatorNamespace; + /** + * @var string|Expression + */ + public $excludeIf; + public function setDiscriminator(string $fieldName, array $map, array $groups = []): void { if (empty($fieldName)) { @@ -203,6 +209,7 @@ public function merge(MergeableInterface $object): void if (!$object instanceof ClassMetadata) { throw new InvalidMetadataException('$object must be an instance of ClassMetadata.'); } + parent::merge($object); $this->preSerializeMethods = array_merge($this->preSerializeMethods, $object->preSerializeMethods); @@ -210,6 +217,10 @@ public function merge(MergeableInterface $object): void $this->postDeserializeMethods = array_merge($this->postDeserializeMethods, $object->postDeserializeMethods); $this->xmlRootName = $object->xmlRootName; $this->xmlRootNamespace = $object->xmlRootNamespace; + if (null !== $object->excludeIf) { + $this->excludeIf = $object->excludeIf; + } + $this->xmlNamespaces = array_merge($this->xmlNamespaces, $object->xmlNamespaces); if ($object->accessorOrder) { @@ -237,6 +248,7 @@ public function merge(MergeableInterface $object): void $this->discriminatorFieldName = $object->discriminatorFieldName; $this->discriminatorMap = $object->discriminatorMap; $this->discriminatorBaseClass = $object->discriminatorBaseClass; + $this->discriminatorGroups = $object->discriminatorGroups; } $this->handleDiscriminatorProperty(); @@ -287,6 +299,7 @@ public function serialize() $this->discriminatorValue, $this->discriminatorMap, $this->discriminatorGroups, + $this->excludeIf, parent::serialize(), 'discriminatorGroups' => $this->discriminatorGroups, 'xmlDiscriminatorAttribute' => $this->xmlDiscriminatorAttribute, @@ -327,12 +340,14 @@ public function unserialize($str) $this->discriminatorValue, $this->discriminatorMap, $this->discriminatorGroups, + $this->excludeIf, $parentStr, ] = $unserialized; if (isset($unserialized['discriminatorGroups'])) { $this->discriminatorGroups = $unserialized['discriminatorGroups']; } + if (isset($unserialized['usingExpression'])) { $this->usingExpression = $unserialized['usingExpression']; } @@ -366,7 +381,8 @@ public function unserialize($str) private function handleDiscriminatorProperty(): void { - if ($this->discriminatorMap + if ( + $this->discriminatorMap && !$this->getReflection()->isAbstract() && !$this->getReflection()->isInterface() ) { @@ -380,7 +396,8 @@ private function handleDiscriminatorProperty(): void $this->discriminatorValue = $typeValue; - if (isset($this->propertyMetadata[$this->discriminatorFieldName]) + if ( + isset($this->propertyMetadata[$this->discriminatorFieldName]) && !$this->propertyMetadata[$this->discriminatorFieldName] instanceof StaticPropertyMetadata ) { throw new InvalidMetadataException(sprintf( diff --git a/src/Metadata/Driver/AbstractDoctrineTypeDriver.php b/src/Metadata/Driver/AbstractDoctrineTypeDriver.php index 249ade7a4..0cb0b4a38 100644 --- a/src/Metadata/Driver/AbstractDoctrineTypeDriver.php +++ b/src/Metadata/Driver/AbstractDoctrineTypeDriver.php @@ -4,8 +4,8 @@ namespace JMS\Serializer\Metadata\Driver; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\Common\Persistence\Mapping\ClassMetadata as DoctrineClassMetadata; +use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\Mapping\ClassMetadata as DoctrineClassMetadata; use JMS\Serializer\Metadata\ClassMetadata; use JMS\Serializer\Metadata\ExpressionPropertyMetadata; use JMS\Serializer\Metadata\PropertyMetadata; @@ -79,8 +79,8 @@ public function __construct(DriverInterface $delegate, ManagerRegistry $registry public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadata { - /** @var ClassMetadata $classMetadata */ $classMetadata = $this->delegate->loadMetadataForClass($class); + \assert($classMetadata instanceof ClassMetadata); // Abort if the given class is not a mapped entity if (!$doctrineMetadata = $this->tryLoadingDoctrineMetadata($class->name)) { @@ -92,8 +92,6 @@ public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadat // We base our scan on the internal driver's property list so that we // respect any internal white/blacklisting like in the AnnotationDriver foreach ($classMetadata->propertyMetadata as $key => $propertyMetadata) { - /** @var $propertyMetadata PropertyMetadata */ - // If the inner driver provides a type, don't guess anymore. if ($propertyMetadata->type || $this->isVirtualProperty($propertyMetadata)) { continue; diff --git a/src/Metadata/Driver/AnnotationDriver.php b/src/Metadata/Driver/AnnotationDriver.php index 2e83f5842..013d0022e 100644 --- a/src/Metadata/Driver/AnnotationDriver.php +++ b/src/Metadata/Driver/AnnotationDriver.php @@ -99,7 +99,11 @@ public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadat } elseif ($annot instanceof XmlNamespace) { $classMetadata->registerNamespace($annot->uri, $annot->prefix); } elseif ($annot instanceof Exclude) { - $excludeAll = true; + if (null !== $annot->if) { + $classMetadata->excludeIf = $this->parseExpression($annot->if); + } else { + $excludeAll = true; + } } elseif ($annot instanceof AccessType) { $classAccessType = $annot->type; } elseif ($annot instanceof ReadOnly) { @@ -158,6 +162,7 @@ public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadat if ($property->class !== $name || (isset($property->info) && $property->info['class'] !== $name)) { continue; } + $propertiesMetadata[] = new PropertyMetadata($name, $property->getName()); $propertiesAnnotations[] = $this->reader->getPropertyAnnotations($property); } @@ -218,8 +223,6 @@ public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadat } elseif ($annot instanceof XmlValue) { $propertyMetadata->xmlValue = true; $propertyMetadata->xmlElementCData = $annot->cdata; - } elseif ($annot instanceof XmlElement) { - $propertyMetadata->xmlElementCData = $annot->cdata; } elseif ($annot instanceof AccessType) { $accessType = $annot->type; } elseif ($annot instanceof ReadOnly) { @@ -265,7 +268,8 @@ public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadat } } - if ((ExclusionPolicy::NONE === $exclusionPolicy && !$isExclude) + if ( + (ExclusionPolicy::NONE === $exclusionPolicy && !$isExclude) || (ExclusionPolicy::ALL === $exclusionPolicy && $isExpose) ) { $propertyMetadata->setAccessor($accessType, $accessor[0], $accessor[1]); diff --git a/src/Metadata/Driver/DocBlockDriver.php b/src/Metadata/Driver/DocBlockDriver.php new file mode 100644 index 000000000..3df37db8f --- /dev/null +++ b/src/Metadata/Driver/DocBlockDriver.php @@ -0,0 +1,86 @@ +delegate = $delegate; + $this->typeParser = $typeParser ?: new Parser(); + $this->docBlockTypeResolver = new DocBlockTypeResolver(); + } + + public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata + { + $classMetadata = $this->delegate->loadMetadataForClass($class); + \assert($classMetadata instanceof SerializerClassMetadata); + + if (null === $classMetadata) { + return null; + } + + // We base our scan on the internal driver's property list so that we + // respect any internal white/blacklisting like in the AnnotationDriver + foreach ($classMetadata->propertyMetadata as $key => $propertyMetadata) { + // If the inner driver provides a type, don't guess anymore. + if ($propertyMetadata->type || $this->isVirtualProperty($propertyMetadata)) { + continue; + } + + try { + $propertyReflection = $this->getReflection($propertyMetadata); + + $type = $this->docBlockTypeResolver->getPropertyDocblockTypeHint($propertyReflection); + if ($type) { + $propertyMetadata->setType($this->typeParser->parse($type)); + } + } catch (ReflectionException $e) { + continue; + } + } + + return $classMetadata; + } + + private function isVirtualProperty(PropertyMetadata $propertyMetadata): bool + { + return $propertyMetadata instanceof VirtualPropertyMetadata + || $propertyMetadata instanceof StaticPropertyMetadata + || $propertyMetadata instanceof ExpressionPropertyMetadata; + } + + private function getReflection(PropertyMetadata $propertyMetadata): ReflectionProperty + { + return new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name); + } +} diff --git a/src/Metadata/Driver/DocBlockDriverFactory.php b/src/Metadata/Driver/DocBlockDriverFactory.php new file mode 100644 index 000000000..676dfe3b3 --- /dev/null +++ b/src/Metadata/Driver/DocBlockDriverFactory.php @@ -0,0 +1,35 @@ +driverFactoryToDecorate = $driverFactoryToDecorate; + $this->typeParser = $typeParser; + } + + public function createDriver(array $metadataDirs, Reader $annotationReader): DriverInterface + { + $driver = $this->driverFactoryToDecorate->createDriver($metadataDirs, $annotationReader); + + return new DocBlockDriver($driver, $this->typeParser); + } +} diff --git a/src/Metadata/Driver/DocBlockTypeResolver.php b/src/Metadata/Driver/DocBlockTypeResolver.php new file mode 100644 index 000000000..fffe51797 --- /dev/null +++ b/src/Metadata/Driver/DocBlockTypeResolver.php @@ -0,0 +1,289 @@ +phpDocParser = new PhpDocParser($typeParser, $constExprParser); + $this->lexer = new Lexer(); + } + + /** + * Attempts to retrieve additional type information from a PhpDoc block. Throws in case of ambiguous type + * information and will return null if no helpful type information could be retrieved. + * + * @param \ReflectionProperty $reflectionProperty + * + * @return string|null + */ + public function getPropertyDocblockTypeHint(\ReflectionProperty $reflectionProperty): ?string + { + if (!$reflectionProperty->getDocComment()) { + return null; + } + + // First we tokenize the PhpDoc comment and parse the tokens into a PhpDocNode. + $tokens = $this->lexer->tokenize($reflectionProperty->getDocComment()); + $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens)); + + // Then we retrieve a flattened list of annotated types excluding null. + $varTagValues = $phpDocNode->getVarTagValues(); + $types = $this->flattenVarTagValueTypes($varTagValues); + $typesWithoutNull = $this->filterNullFromTypes($types); + + // The PhpDoc does not contain additional type information. + if (0 === count($typesWithoutNull)) { + return null; + } + + // The PhpDoc contains multiple non-null types which produces ambiguity when deserializing. + if (count($typesWithoutNull) > 1) { + $typeHint = implode('|', array_map(static function (TypeNode $type) { + return (string) $type; + }, $types)); + + throw new \InvalidArgumentException(sprintf("Can't use union type %s for collection in %s:%s", $typeHint, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName())); + } + + // Only one type is left, so we only need to differentiate between arrays, generics and other types. + $type = $typesWithoutNull[0]; + + // Simple array without concrete type: array + if ($this->isSimpleType($type, 'array')) { + return null; + } + + // Normal array syntax: Product[] | \Foo\Bar\Product[] + if ($type instanceof ArrayTypeNode) { + $resolvedType = $this->resolveTypeFromTypeNode($type->type, $reflectionProperty); + + return 'array<' . $resolvedType . '>'; + } + + // Generic array syntax: array | array<\Foo\Bar\Product> | array + if ($type instanceof GenericTypeNode) { + if (!$this->isSimpleType($type->type, 'array')) { + throw new \InvalidArgumentException(sprintf("Can't use non-array generic type %s for collection in %s:%s", (string) $type->type, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName())); + } + + $resolvedTypes = array_map(function (TypeNode $node) use ($reflectionProperty) { + return $this->resolveTypeFromTypeNode($node, $reflectionProperty); + }, $type->genericTypes); + + return 'array<' . implode(',', $resolvedTypes) . '>'; + } + + // Primitives and class names: Collection | \Foo\Bar\Product | string + return $this->resolveTypeFromTypeNode($type, $reflectionProperty); + } + + /** + * Returns a flat list of types of the given var tags. Union types are flattened as well. + * + * @param VarTagValueNode[] $varTagValues + * + * @return TypeNode[] + */ + private function flattenVarTagValueTypes(array $varTagValues): array + { + return array_merge(...array_map(static function (VarTagValueNode $node) { + if ($node->type instanceof UnionTypeNode) { + return $node->type->types; + } + + return [$node->type]; + }, $varTagValues)); + } + + /** + * Filters the null type from the given types array. If no null type is found, the array is returned unchanged. + * + * @param TypeNode[] $types + * + * @return TypeNode[] + */ + private function filterNullFromTypes(array $types): array + { + return array_filter(array_map(function (TypeNode $node) { + return $this->isNullType($node) ? null : $node; + }, $types)); + } + + /** + * Determines if the given type is a null type. + * + * @param TypeNode $typeNode + * + * @return bool + */ + private function isNullType(TypeNode $typeNode): bool + { + return $this->isSimpleType($typeNode, 'null'); + } + + /** + * Determines if the given node represents a simple type. + * + * @param TypeNode $typeNode + * @param string $simpleType + * + * @return bool + */ + private function isSimpleType(TypeNode $typeNode, string $simpleType): bool + { + return $typeNode instanceof IdentifierTypeNode && $typeNode->name === $simpleType; + } + + /** + * Attempts to resolve the fully qualified type from the given node. If the node is not suitable for type + * retrieval, an exception is thrown. + * + * @param TypeNode $typeNode + * @param \ReflectionProperty $reflectionProperty + * + * @return string + * + * @throws \InvalidArgumentException + */ + private function resolveTypeFromTypeNode(TypeNode $typeNode, \ReflectionProperty $reflectionProperty): string + { + if (!($typeNode instanceof IdentifierTypeNode)) { + throw new \InvalidArgumentException(sprintf("Can't use unsupported type %s for collection in %s:%s", (string) $typeNode, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName())); + } + + return $this->resolveType($typeNode->name, $reflectionProperty); + } + + private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, \ReflectionProperty $reflectionProperty): string + { + if ($this->isClassOrInterface($typeHint)) { + return $typeHint; + } + + $expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint; + if ($this->isClassOrInterface($expandedClassName)) { + return $expandedClassName; + } + + $classContents = file_get_contents($declaringClass->getFileName()); + $foundUseStatements = $this->gatherGroupUseStatements($classContents); + $foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements); + + foreach ($foundUseStatements as $statementClassName) { + if ($alias = explode('as', $statementClassName)) { + if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) { + return trim($alias[0]); + } + } + + if ($this->endsWith($statementClassName, $typeHint)) { + return $statementClassName; + } + } + + throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflectionProperty->getName())); + } + + private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool + { + $typeHintToSearchFor = '\\' . $typeHintToSearchFor; + + return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor; + } + + private function isPrimitiveType(string $type): bool + { + return in_array($type, ['int', 'float', 'bool', 'string']); + } + + private function hasGlobalNamespacePrefix(string $typeHint): bool + { + return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0]; + } + + private function gatherGroupUseStatements(string $classContents): array + { + $foundUseStatements = []; + preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements); + for ($useStatementIndex = 0; $useStatementIndex < count($foundGroupUseStatements[0]); $useStatementIndex++) { + foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) { + $foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement); + } + } + + return $foundUseStatements; + } + + private function gatherSingleUseStatements(string $classContents): array + { + $foundUseStatements = []; + preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements); + for ($useStatementIndex = 0; $useStatementIndex < count($foundSingleUseStatements[0]); $useStatementIndex++) { + $foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]); + } + + return $foundUseStatements; + } + + private function getDeclaringClassOrTrait(\ReflectionProperty $reflectionProperty): \ReflectionClass + { + foreach ($reflectionProperty->getDeclaringClass()->getTraits() as $trait) { + foreach ($trait->getProperties() as $traitProperty) { + if ($traitProperty->getName() === $reflectionProperty->getName()) { + return $this->getDeclaringClassOrTrait($traitProperty); + } + } + } + + return $reflectionProperty->getDeclaringClass(); + } + + private function resolveType(string $typeHint, \ReflectionProperty $reflectionProperty): string + { + if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) { + $typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflectionProperty), $reflectionProperty); + } + + return ltrim($typeHint, '\\'); + } + + private function isClassOrInterface(string $typeHint): bool + { + return class_exists($typeHint) || interface_exists($typeHint); + } +} diff --git a/src/Metadata/Driver/DoctrinePHPCRTypeDriver.php b/src/Metadata/Driver/DoctrinePHPCRTypeDriver.php index f3e782b6f..d402a449f 100644 --- a/src/Metadata/Driver/DoctrinePHPCRTypeDriver.php +++ b/src/Metadata/Driver/DoctrinePHPCRTypeDriver.php @@ -4,7 +4,7 @@ namespace JMS\Serializer\Metadata\Driver; -use Doctrine\Common\Persistence\Mapping\ClassMetadata as DoctrineClassMetadata; +use Doctrine\Persistence\Mapping\ClassMetadata as DoctrineClassMetadata; use JMS\Serializer\Metadata\PropertyMetadata; /** @@ -23,9 +23,11 @@ protected function hideProperty(DoctrineClassMetadata $doctrineMetadata, Propert protected function setPropertyType(DoctrineClassMetadata $doctrineMetadata, PropertyMetadata $propertyMetadata): void { $propertyName = $propertyMetadata->name; - if ($doctrineMetadata->hasField($propertyName) + if ( + $doctrineMetadata->hasField($propertyName) && ($typeOfFiled = $doctrineMetadata->getTypeOfField($propertyName)) - && ($fieldType = $this->normalizeFieldType($typeOfFiled))) { + && ($fieldType = $this->normalizeFieldType($typeOfFiled)) + ) { $field = $doctrineMetadata->getFieldMapping($propertyName); if (!empty($field['multivalue'])) { $fieldType = 'array'; diff --git a/src/Metadata/Driver/DoctrineTypeDriver.php b/src/Metadata/Driver/DoctrineTypeDriver.php index 5a729a286..d105e923c 100644 --- a/src/Metadata/Driver/DoctrineTypeDriver.php +++ b/src/Metadata/Driver/DoctrineTypeDriver.php @@ -4,7 +4,7 @@ namespace JMS\Serializer\Metadata\Driver; -use Doctrine\Common\Persistence\Mapping\ClassMetadata as DoctrineClassMetadata; +use Doctrine\Persistence\Mapping\ClassMetadata as DoctrineClassMetadata; use JMS\Serializer\Metadata\ClassMetadata; use JMS\Serializer\Metadata\PropertyMetadata; @@ -16,7 +16,8 @@ class DoctrineTypeDriver extends AbstractDoctrineTypeDriver { protected function setDiscriminator(DoctrineClassMetadata $doctrineMetadata, ClassMetadata $classMetadata): void { - if (empty($classMetadata->discriminatorMap) && !$classMetadata->discriminatorDisabled + if ( + empty($classMetadata->discriminatorMap) && !$classMetadata->discriminatorDisabled && !empty($doctrineMetadata->discriminatorMap) && $doctrineMetadata->isRootEntity() ) { $classMetadata->setDiscriminator( @@ -29,9 +30,11 @@ protected function setDiscriminator(DoctrineClassMetadata $doctrineMetadata, Cla protected function setPropertyType(DoctrineClassMetadata $doctrineMetadata, PropertyMetadata $propertyMetadata): void { $propertyName = $propertyMetadata->name; - if ($doctrineMetadata->hasField($propertyName) + if ( + $doctrineMetadata->hasField($propertyName) && ($typeOfFiled = $doctrineMetadata->getTypeOfField($propertyName)) - && ($fieldType = $this->normalizeFieldType($typeOfFiled))) { + && ($fieldType = $this->normalizeFieldType($typeOfFiled)) + ) { $propertyMetadata->setType($this->typeParser->parse($fieldType)); } elseif ($doctrineMetadata->hasAssociation($propertyName)) { $targetEntity = $doctrineMetadata->getAssociationTargetClass($propertyName); diff --git a/src/Metadata/Driver/TypedPropertiesDriver.php b/src/Metadata/Driver/TypedPropertiesDriver.php new file mode 100644 index 000000000..9e6ba2727 --- /dev/null +++ b/src/Metadata/Driver/TypedPropertiesDriver.php @@ -0,0 +1,118 @@ +delegate = $delegate; + $this->typeParser = $typeParser ?: new Parser(); + $this->whiteList = array_merge($whiteList, $this->getDefaultWhiteList()); + } + + private function getDefaultWhiteList(): array + { + return [ + 'int', + 'float', + 'bool', + 'boolean', + 'string', + 'double', + 'iterable', + 'resource', + ]; + } + + public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata + { + $classMetadata = $this->delegate->loadMetadataForClass($class); + \assert($classMetadata instanceof SerializerClassMetadata); + + if (null === $classMetadata) { + return null; + } + + // We base our scan on the internal driver's property list so that we + // respect any internal white/blacklisting like in the AnnotationDriver + foreach ($classMetadata->propertyMetadata as $key => $propertyMetadata) { + // If the inner driver provides a type, don't guess anymore. + if ($propertyMetadata->type || $this->isVirtualProperty($propertyMetadata)) { + continue; + } + + try { + $propertyReflection = $this->getReflection($propertyMetadata); + if ($this->shouldTypeHint($propertyReflection)) { + $type = $propertyReflection->getType()->getName(); + + $propertyMetadata->setType($this->typeParser->parse($type)); + } + } catch (ReflectionException $e) { + continue; + } + } + + return $classMetadata; + } + + private function isVirtualProperty(PropertyMetadata $propertyMetadata): bool + { + return $propertyMetadata instanceof VirtualPropertyMetadata + || $propertyMetadata instanceof StaticPropertyMetadata + || $propertyMetadata instanceof ExpressionPropertyMetadata; + } + + private function getReflection(PropertyMetadata $propertyMetadata): ReflectionProperty + { + return new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name); + } + + private function shouldTypeHint(ReflectionProperty $propertyReflection): bool + { + if (null === $propertyReflection->getType()) { + return false; + } + + if (in_array($propertyReflection->getType()->getName(), $this->whiteList, true)) { + return true; + } + + return class_exists($propertyReflection->getType()->getName()) + || interface_exists($propertyReflection->getType()->getName()); + } +} diff --git a/src/Metadata/Driver/XmlDriver.php b/src/Metadata/Driver/XmlDriver.php index 32bf70c9f..9eecf5502 100644 --- a/src/Metadata/Driver/XmlDriver.php +++ b/src/Metadata/Driver/XmlDriver.php @@ -57,6 +57,7 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): if (!$elems = $elem->xpath("./class[@name = '" . $name . "']")) { throw new InvalidMetadataException(sprintf('Could not find class %s inside XML element.', $name)); } + $elem = reset($elems); $metadata->fileResources[] = $path; @@ -64,9 +65,15 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): if (false !== $fileResource) { $metadata->fileResources[] = $fileResource; } + $exclusionPolicy = strtoupper((string) $elem->attributes()->{'exclusion-policy'}) ?: 'NONE'; $exclude = $elem->attributes()->exclude; $excludeAll = null !== $exclude ? 'true' === strtolower((string) $exclude) : false; + + if (null !== $excludeIf = $elem->attributes()->{'exclude-if'}) { + $metadata->excludeIf = $this->parseExpression((string) $excludeIf); + } + $classAccessType = (string) ($elem->attributes()->{'access-type'} ?: PropertyMetadata::ACCESS_TYPE_PROPERTY); $propertiesMetadata = []; @@ -83,6 +90,7 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): if (null !== $xmlRootNamespace = $elem->attributes()->{'xml-root-namespace'}) { $metadata->xmlRootNamespace = (string) $xmlRootNamespace; } + if (null !== $xmlRootPrefix = $elem->attributes()->{'xml-root-prefix'}) { $metadata->xmlRootPrefix = (string) $xmlRootPrefix; } @@ -106,6 +114,7 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): foreach ($elem->xpath('./discriminator-groups/group') as $entry) { $discriminatorGroups[] = (string) $entry; } + $metadata->setDiscriminator($discriminatorFieldName, $discriminatorMap, $discriminatorGroups); } @@ -127,9 +136,11 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): if (isset($xmlDiscriminator->attributes()->attribute)) { $metadata->xmlDiscriminatorAttribute = 'true' === (string) $xmlDiscriminator->attributes()->attribute; } + if (isset($xmlDiscriminator->attributes()->cdata)) { $metadata->xmlDiscriminatorCData = 'true' === (string) $xmlDiscriminator->attributes()->cdata; } + if (isset($xmlDiscriminator->attributes()->namespace)) { $metadata->xmlDiscriminatorNamespace = (string) $xmlDiscriminator->attributes()->namespace; } @@ -146,6 +157,7 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): if (!isset($method->attributes()->method)) { throw new InvalidMetadataException('The method attribute must be set for all virtual-property elements.'); } + $virtualPropertyMetadata = new VirtualPropertyMetadata($name, (string) $method->attributes()->method); } @@ -159,16 +171,18 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): continue; } - $propertiesMetadata[] = new PropertyMetadata($name, $pName = $property->getName()); - $pElems = $elem->xpath("./property[@name = '" . $pName . "']"); + $pName = $property->getName(); + $propertiesMetadata[] = new PropertyMetadata($name, $pName); + $pElems = $elem->xpath("./property[@name = '" . $pName . "']"); $propertiesNodes[] = $pElems ? reset($pElems) : null; } foreach ($propertiesMetadata as $propertyKey => $pMetadata) { $isExclude = false; $isExpose = $pMetadata instanceof VirtualPropertyMetadata - || $pMetadata instanceof ExpressionPropertyMetadata; + || $pMetadata instanceof ExpressionPropertyMetadata + || isset($propertiesNodes[$propertyKey]); $pElem = $propertiesNodes[$propertyKey]; if (!empty($pElem)) { @@ -329,7 +343,8 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): $pMetadata->name = (string) $name; } - if ((ExclusionPolicy::NONE === (string) $exclusionPolicy && !$isExclude) + if ( + (ExclusionPolicy::NONE === (string) $exclusionPolicy && !$isExclude) || (ExclusionPolicy::ALL === (string) $exclusionPolicy && $isExpose) ) { $metadata->addPropertyMetadata($pMetadata); @@ -341,6 +356,7 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): if (!isset($method->attributes()->type)) { throw new InvalidMetadataException('The type attribute must be set for all callback-method elements.'); } + if (!isset($method->attributes()->name)) { throw new InvalidMetadataException('The name attribute must be set for all callback-method elements.'); } @@ -362,6 +378,7 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): if (!isset($method->attributes()->format)) { throw new InvalidMetadataException('The format attribute must be set for "handler" callback methods.'); } + if (!isset($method->attributes()->direction)) { throw new InvalidMetadataException('The direction attribute must be set for "handler" callback methods.'); } diff --git a/src/Metadata/Driver/YamlDriver.php b/src/Metadata/Driver/YamlDriver.php index d5f8b5946..e7880c6a1 100644 --- a/src/Metadata/Driver/YamlDriver.php +++ b/src/Metadata/Driver/YamlDriver.php @@ -109,11 +109,17 @@ protected function loadMetadataFromFile(ReflectionClass $class, string $file): ? $exclusionPolicy = isset($config['exclusion_policy']) ? strtoupper($config['exclusion_policy']) : 'NONE'; $excludeAll = isset($config['exclude']) ? (bool) $config['exclude'] : false; + + if (isset($config['exclude_if'])) { + $metadata->excludeIf = $this->parseExpression((string) $config['exclude_if']); + } + $classAccessType = $config['access_type'] ?? PropertyMetadata::ACCESS_TYPE_PROPERTY; $readOnlyClass = isset($config['read_only']) ? (bool) $config['read_only'] : false; $this->addClassProperties($metadata, $config); $propertiesMetadata = []; + $propertiesData = []; if (array_key_exists('virtual_properties', $config)) { foreach ($config['virtual_properties'] as $methodName => $propertySettings) { if (isset($propertySettings['exp'])) { @@ -129,13 +135,12 @@ protected function loadMetadataFromFile(ReflectionClass $class, string $file): ? 'The method ' . $methodName . ' not found in class ' . $class->name ); } + $virtualPropertyMetadata = new VirtualPropertyMetadata($name, $methodName); } - $pName = !empty($propertySettings['name']) ? $propertySettings['name'] : $virtualPropertyMetadata->name; - - $propertiesMetadata[$pName] = $virtualPropertyMetadata; - $config['properties'][$pName] = $propertySettings; + $propertiesMetadata[] = $virtualPropertyMetadata; + $propertiesData[] = $propertySettings; } } @@ -146,18 +151,20 @@ protected function loadMetadataFromFile(ReflectionClass $class, string $file): ? } $pName = $property->getName(); - $propertiesMetadata[$pName] = new PropertyMetadata($name, $pName); + $propertiesMetadata[] = new PropertyMetadata($name, $pName); + $propertiesData[] = !empty($config['properties']) && true === array_key_exists($pName, $config['properties']) + ? (array) $config['properties'][$pName] + : null; } - foreach ($propertiesMetadata as $pName => $pMetadata) { + foreach ($propertiesMetadata as $propertyKey => $pMetadata) { $isExclude = false; $isExpose = $pMetadata instanceof VirtualPropertyMetadata || $pMetadata instanceof ExpressionPropertyMetadata - || (isset($config['properties']) && array_key_exists($pName, $config['properties'])); - - if (isset($config['properties'][$pName])) { - $pConfig = $config['properties'][$pName]; + || isset($propertiesData[$propertyKey]); + $pConfig = $propertiesData[$propertyKey]; + if (!empty($pConfig)) { if (isset($pConfig['exclude'])) { $isExclude = (bool) $pConfig['exclude']; } @@ -304,15 +311,12 @@ protected function loadMetadataFromFile(ReflectionClass $class, string $file): ? $metadata->isMap = $metadata->isMap || PropertyMetadata::isCollectionMap($pMetadata->type); } - if (isset($config['properties'][$pName])) { - $pConfig = $config['properties'][$pName]; - - if (isset($pConfig['name'])) { - $pMetadata->name = (string) $pConfig['name']; - } + if (!empty($pConfig) && !empty($pConfig['name'])) { + $pMetadata->name = (string) $pConfig['name']; } - if ((ExclusionPolicy::NONE === $exclusionPolicy && !$isExclude) + if ( + (ExclusionPolicy::NONE === $exclusionPolicy && !$isExclude) || (ExclusionPolicy::ALL === $exclusionPolicy && $isExpose) ) { $metadata->addPropertyMetadata($pMetadata); @@ -326,9 +330,11 @@ protected function loadMetadataFromFile(ReflectionClass $class, string $file): ? if (isset($cConfig['pre_serialize'])) { $metadata->preSerializeMethods = $this->getCallbackMetadata($class, $cConfig['pre_serialize']); } + if (isset($cConfig['post_serialize'])) { $metadata->postSerializeMethods = $this->getCallbackMetadata($class, $cConfig['post_serialize']); } + if (isset($cConfig['post_deserialize'])) { $metadata->postDeserializeMethods = $this->getCallbackMetadata($class, $cConfig['post_deserialize']); } @@ -394,6 +400,7 @@ private function addClassProperties(ClassMetadata $metadata, array $config): voi 'The "map" attribute must be set, and be an array for discriminators.' ); } + $groups = $config['discriminator']['groups'] ?? []; $metadata->setDiscriminator( $config['discriminator']['field_name'], @@ -404,10 +411,12 @@ private function addClassProperties(ClassMetadata $metadata, array $config): voi if (isset($config['discriminator']['xml_attribute'])) { $metadata->xmlDiscriminatorAttribute = (bool) $config['discriminator']['xml_attribute']; } + if (isset($config['discriminator']['xml_element'])) { if (isset($config['discriminator']['xml_element']['cdata'])) { $metadata->xmlDiscriminatorCData = (bool) $config['discriminator']['xml_element']['cdata']; } + if (isset($config['discriminator']['xml_element']['namespace'])) { $metadata->xmlDiscriminatorNamespace = (string) $config['discriminator']['xml_element']['namespace']; } diff --git a/src/Metadata/ExpressionPropertyMetadata.php b/src/Metadata/ExpressionPropertyMetadata.php index d3ffe390d..697f2a966 100644 --- a/src/Metadata/ExpressionPropertyMetadata.php +++ b/src/Metadata/ExpressionPropertyMetadata.php @@ -60,6 +60,7 @@ protected function unserializeProperties(string $str): string $this->expression, $parentStr, ] = unserialize($str); + return parent::unserializeProperties($parentStr); } } diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index cd2c8734c..f7d0f513c 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -5,6 +5,7 @@ namespace JMS\Serializer\Metadata; use JMS\Serializer\Exception\InvalidMetadataException; +use JMS\Serializer\Expression\Expression; use Metadata\PropertyMetadata as BasePropertyMetadata; class PropertyMetadata extends BasePropertyMetadata @@ -124,7 +125,7 @@ class PropertyMetadata extends BasePropertyMetadata public $maxDepth = null; /** - * @var string + * @var string|Expression */ public $excludeIf = null; @@ -286,15 +287,19 @@ protected function unserializeProperties(string $str): string if (isset($unserialized['xmlEntryNamespace'])) { $this->xmlEntryNamespace = $unserialized['xmlEntryNamespace']; } + if (isset($unserialized['xmlCollectionSkipWhenEmpty'])) { $this->xmlCollectionSkipWhenEmpty = $unserialized['xmlCollectionSkipWhenEmpty']; } + if (isset($unserialized['excludeIf'])) { $this->excludeIf = $unserialized['excludeIf']; } + if (isset($unserialized['skipWhenEmpty'])) { $this->skipWhenEmpty = $unserialized['skipWhenEmpty']; } + if (isset($unserialized['forceReflectionAccess'])) { $this->forceReflectionAccess = $unserialized['forceReflectionAccess']; } diff --git a/src/Metadata/StaticPropertyMetadata.php b/src/Metadata/StaticPropertyMetadata.php index f1b8e746c..6d0079709 100644 --- a/src/Metadata/StaticPropertyMetadata.php +++ b/src/Metadata/StaticPropertyMetadata.php @@ -65,6 +65,7 @@ protected function unserializeProperties(string $str): string $this->value, $parentStr, ] = unserialize($str); + return parent::unserializeProperties($parentStr); } } diff --git a/src/Naming/CamelCaseNamingStrategy.php b/src/Naming/CamelCaseNamingStrategy.php index 5ca0608b7..71d451715 100644 --- a/src/Naming/CamelCaseNamingStrategy.php +++ b/src/Naming/CamelCaseNamingStrategy.php @@ -29,9 +29,6 @@ public function __construct(string $separator = '_', bool $lowerCase = true) $this->lowerCase = $lowerCase; } - /** - * {@inheritDoc} - */ public function translateName(PropertyMetadata $property): string { $name = preg_replace('/[A-Z]+/', $this->separator . '\\0', $property->name); diff --git a/src/Naming/SerializedNameAnnotationStrategy.php b/src/Naming/SerializedNameAnnotationStrategy.php index facf17593..702b66cf8 100644 --- a/src/Naming/SerializedNameAnnotationStrategy.php +++ b/src/Naming/SerializedNameAnnotationStrategy.php @@ -23,9 +23,6 @@ public function __construct(PropertyNamingStrategyInterface $namingStrategy) $this->delegate = $namingStrategy; } - /** - * {@inheritDoc} - */ public function translateName(PropertyMetadata $property): string { if (null !== $name = $property->serializedName) { diff --git a/src/SerializationContext.php b/src/SerializationContext.php index bd3fa99d0..24073536b 100644 --- a/src/SerializationContext.php +++ b/src/SerializationContext.php @@ -43,6 +43,8 @@ public function initialize(string $format, VisitorInterface $visitor, GraphNavig */ public function setSerializeNull(bool $bool): self { + $this->assertMutable(); + $this->serializeNull = $bool; return $this; @@ -65,6 +67,7 @@ public function startVisiting($object): void if (!\is_object($object)) { return; } + $this->visitingSet->attach($object); $this->visitingStack->push($object); } @@ -77,6 +80,7 @@ public function stopVisiting($object): void if (!\is_object($object)) { return; } + $this->visitingSet->detach($object); $poppedObject = $this->visitingStack->pop(); @@ -131,7 +135,6 @@ public function getVisitingStack(): \SplStack return $this->visitingStack; } - public function getVisitingSet(): \SplObjectStorage { return $this->visitingSet; @@ -142,8 +145,11 @@ public function getVisitingSet(): \SplObjectStorage */ public function setInitialType(string $type): self { + $this->assertMutable(); + $this->initialType = $type; $this->setAttribute('initial_type', $type); + return $this; } diff --git a/src/Serializer.php b/src/Serializer.php index bae4c8ca1..030d9f59d 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -38,12 +38,12 @@ final class Serializer implements SerializerInterface, ArrayTransformerInterface /** * @var SerializationVisitorFactory[] */ - private $serializationVisitors = []; + private $serializationVisitors; /** * @var DeserializationVisitorFactory[] */ - private $deserializationVisitors = []; + private $deserializationVisitors; /** * @var SerializationContextFactoryInterface @@ -109,6 +109,7 @@ private function findInitialType(?string $type, SerializationContext $context): } elseif ($context->hasAttribute('initial_type')) { return $context->getAttribute('initial_type'); } + return null; } @@ -160,6 +161,7 @@ public function serialize($data, string $format, ?SerializationContext $context $type = $this->findInitialType($type, $context); $result = $this->visit($navigator, $visitor, $context, $data, $format, $type); + return $visitor->getResult($result); } @@ -246,6 +248,7 @@ private function visit(GraphNavigatorInterface $navigator, VisitorInterface $vis if (null !== $type) { $type = $this->typeParser->parse($type); } + return $navigator->accept($data, $type); } @@ -259,6 +262,7 @@ private function convertArrayObjects($data) if ($data instanceof \ArrayObject || $data instanceof \stdClass) { $data = (array) $data; } + if (\is_array($data)) { foreach ($data as $k => $v) { $data[$k] = $this->convertArrayObjects($v); diff --git a/src/SerializerBuilder.php b/src/SerializerBuilder.php index 77eba38f4..a315889a5 100644 --- a/src/SerializerBuilder.php +++ b/src/SerializerBuilder.php @@ -34,6 +34,7 @@ use JMS\Serializer\Handler\HandlerRegistryInterface; use JMS\Serializer\Handler\IteratorHandler; use JMS\Serializer\Handler\StdClassHandler; +use JMS\Serializer\Metadata\Driver\DocBlockDriverFactory; use JMS\Serializer\Naming\CamelCaseNamingStrategy; use JMS\Serializer\Naming\PropertyNamingStrategyInterface; use JMS\Serializer\Naming\SerializedNameAnnotationStrategy; @@ -165,6 +166,11 @@ final class SerializerBuilder */ private $metadataCache; + /** + * @var bool + */ + private $docBlockTyperResolver; + /** * @param mixed ...$args * @@ -186,6 +192,7 @@ public function __construct(?HandlerRegistryInterface $handlerRegistry = null, ? if ($handlerRegistry) { $this->handlersConfigured = true; } + if ($eventDispatcher) { $this->listenersConfigured = true; } @@ -194,6 +201,7 @@ public function __construct(?HandlerRegistryInterface $handlerRegistry = null, ? public function setAccessorStrategy(AccessorStrategyInterface $accessorStrategy): self { $this->accessorStrategy = $accessorStrategy; + return $this; } @@ -202,6 +210,7 @@ private function getAccessorStrategy(): AccessorStrategyInterface if (!$this->accessorStrategy) { $this->accessorStrategy = new DefaultAccessorStrategy($this->expressionEvaluator); } + return $this->accessorStrategy; } @@ -238,6 +247,7 @@ public function setCacheDir(string $dir): self if (!is_dir($dir)) { $this->createDir($dir); } + if (!is_writable($dir)) { throw new InvalidArgumentException(sprintf('The cache directory "%s" is not writable.', $dir)); } @@ -496,10 +506,18 @@ public function setDeserializationContextFactory($deserializationContextFactory) public function setMetadataCache(CacheInterface $cache): self { $this->metadataCache = $cache; + return $this; } - public function build(): SerializerInterface + public function setDocBlockTypeResolver(bool $docBlockTypeResolver): self + { + $this->docBlockTyperResolver = $docBlockTypeResolver; + + return $this; + } + + public function build(): Serializer { $annotationReader = $this->annotationReader; if (null === $annotationReader) { @@ -521,6 +539,10 @@ public function build(): SerializerInterface ); } + if ($this->docBlockTyperResolver) { + $this->driverFactory = new DocBlockDriverFactory($this->driverFactory, $this->typeParser); + } + $metadataDriver = $this->driverFactory->createDriver($this->metadataDirs, $annotationReader); $metadataFactory = new MetadataFactory($metadataDriver, null, $this->debug); @@ -545,6 +567,7 @@ public function build(): SerializerInterface $this->addDefaultSerializationVisitors(); $this->addDefaultDeserializationVisitors(); } + $navigatorFactories = [ GraphNavigatorInterface::DIRECTION_SERIALIZATION => $this->getSerializationNavigatorFactory($metadataFactory), GraphNavigatorInterface::DIRECTION_DESERIALIZATION => $this->getDeserializationNavigatorFactory($metadataFactory), diff --git a/src/Type/InnerParser.php b/src/Type/InnerParser.php deleted file mode 100644 index eb05aa448..000000000 --- a/src/Type/InnerParser.php +++ /dev/null @@ -1,81 +0,0 @@ - [ - 'skip' => '\s+', - 'parenthesis_' => '<', - '_parenthesis' => '>', - 'empty_string' => '""|\'\'', - 'number' => '(\+|\-)?(0|[1-9]\d*)(\.\d+)?', - 'null' => 'null', - 'comma' => ',', - 'name' => '(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\\\)*[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*', - 'quote_:quoted_string' => '"', - 'apostrophe_:apostrophed_string' => '\'', - ], - 'quoted_string' => [ - 'quoted_string' => '[^"]+', - '_quote:default' => '"', - ], - 'apostrophed_string' => [ - 'apostrophed_string' => '[^\']+', - '_apostrophe:default' => '\'', - ], - ], - [ - 'type' => new Choice('type', ['simple_type', 'compound_type'], null), - 1 => new Token(1, 'name', null, -1, true), - 2 => new Concatenation(2, [1], '#simple_type'), - 3 => new Token(3, 'number', null, -1, true), - 4 => new Concatenation(4, [3], '#simple_type'), - 5 => new Token(5, 'null', null, -1, true), - 6 => new Concatenation(6, [5], '#simple_type'), - 7 => new Token(7, 'empty_string', null, -1, true), - 8 => new Concatenation(8, [7], '#simple_type'), - 9 => new Token(9, 'quote_', null, -1, false), - 10 => new Token(10, 'quoted_string', null, -1, true), - 11 => new Token(11, '_quote', null, -1, false), - 12 => new Concatenation(12, [9, 10, 11], '#simple_type'), - 13 => new Token(13, 'apostrophe_', null, -1, false), - 14 => new Token(14, 'apostrophed_string', null, -1, true), - 15 => new Token(15, '_apostrophe', null, -1, false), - 16 => new Concatenation(16, [13, 14, 15], '#simple_type'), - 'simple_type' => new Choice('simple_type', [2, 4, 6, 8, 12, 16], null), - 18 => new Token(18, 'name', null, -1, true), - 19 => new Token(19, 'parenthesis_', null, -1, false), - 20 => new Token(20, 'comma', null, -1, false), - 21 => new Concatenation(21, [20, 'type'], '#compound_type'), - 22 => new Repetition(22, 0, -1, 21, null), - 23 => new Token(23, '_parenthesis', null, -1, false), - 'compound_type' => new Concatenation('compound_type', [18, 19, 'type', 22, 23], null), - ], - [] - ); - - $this->getRule('type')->setPPRepresentation(' simple_type() | compound_type()'); - $this->getRule('simple_type')->setDefaultId('#simple_type'); - $this->getRule('simple_type')->setPPRepresentation(' | | | | ::quote_:: ::_quote:: | ::apostrophe_:: ::_apostrophe::'); - $this->getRule('compound_type')->setDefaultId('#compound_type'); - $this->getRule('compound_type')->setPPRepresentation(' ::parenthesis_:: type() ( ::comma:: type() )* ::_parenthesis::'); - } -} diff --git a/src/Type/Lexer.php b/src/Type/Lexer.php new file mode 100644 index 000000000..dba7c8a9f --- /dev/null +++ b/src/Type/Lexer.php @@ -0,0 +1,111 @@ +getType($type); + } catch (\Throwable $e) { + throw new SyntaxError($e->getMessage(), 0, $e); + } + } + + protected function getCatchablePatterns(): array + { + return [ + '[a-z][a-z_\\\\0-9]*', // identifier or qualified name + "'(?:[^']|'')*'", // single quoted strings + '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers + '"(?:[^"]|"")*"', // double quoted strings + '<', + '>', + '\\[', + '\\]', + ]; + } + + protected function getNonCatchablePatterns(): array + { + return ['\s+']; + } + + /** + * {{@inheritDoc}} + */ + protected function getType(&$value) + { + $type = self::T_UNKNOWN; + + switch (true) { + // Recognize numeric values + case is_numeric($value): + if (false !== strpos($value, '.') || false !== stripos($value, 'e')) { + return self::T_FLOAT; + } + + return self::T_INTEGER; + + // Recognize quoted strings + case "'" === $value[0]: + $value = str_replace("''", "'", substr($value, 1, strlen($value) - 2)); + + return self::T_STRING; + + case '"' === $value[0]: + $value = str_replace('""', '"', substr($value, 1, strlen($value) - 2)); + + return self::T_STRING; + + case 'null' === $value: + return self::T_NULL; + + // Recognize identifiers, aliased or qualified names + case ctype_alpha($value[0]) || '\\' === $value[0]: + return self::T_IDENTIFIER; + + case ',' === $value: + return self::T_COMMA; + + case '>' === $value: + return self::T_TYPE_END; + + case '<' === $value: + return self::T_TYPE_START; + + case ']' === $value: + return self::T_ARRAY_END; + + case '[' === $value: + return self::T_ARRAY_START; + + // Default + default: + // Do nothing + } + + return $type; + } +} diff --git a/src/Type/Parser.php b/src/Type/Parser.php index de5a7c3a7..2accb9272 100644 --- a/src/Type/Parser.php +++ b/src/Type/Parser.php @@ -4,32 +4,159 @@ namespace JMS\Serializer\Type; -use Hoa\Exception\Exception; -use Hoa\Visitor\Visit; use JMS\Serializer\Type\Exception\SyntaxError; +/** + * @internal + */ final class Parser implements ParserInterface { - /** @var InnerParser */ - private $parser; + /** + * @var Lexer + */ + private $lexer; - /** @var Visit */ - private $visitor; + /** + * @var bool + */ + private $root = true; - public function __construct() + public function parse(string $string): array { - $this->parser = new InnerParser(); - $this->visitor = new TypeVisitor(); + $this->lexer = new Lexer(); + $this->lexer->setInput($string); + $this->lexer->moveNext(); + + return $this->visit(); + } + + /** + * @return mixed + */ + private function visit() + { + $this->lexer->moveNext(); + + if (!$this->lexer->token) { + throw new SyntaxError( + 'Syntax error, unexpected end of stream' + ); + } + + if (Lexer::T_FLOAT === $this->lexer->token['type']) { + return floatval($this->lexer->token['value']); + } elseif (Lexer::T_INTEGER === $this->lexer->token['type']) { + return intval($this->lexer->token['value']); + } elseif (Lexer::T_NULL === $this->lexer->token['type']) { + return null; + } elseif (Lexer::T_STRING === $this->lexer->token['type']) { + return $this->lexer->token['value']; + } elseif (Lexer::T_IDENTIFIER === $this->lexer->token['type']) { + if ($this->lexer->isNextToken(Lexer::T_TYPE_START)) { + return $this->visitCompoundType(); + } elseif ($this->lexer->isNextToken(Lexer::T_ARRAY_START)) { + return $this->visitArrayType(); + } + + return $this->visitSimpleType(); + } elseif (!$this->root && Lexer::T_ARRAY_START === $this->lexer->token['type']) { + return $this->visitArrayType(); + } + + throw new SyntaxError(sprintf( + 'Syntax error, unexpected "%s" (%s)', + $this->lexer->token['value'], + $this->getConstant($this->lexer->token['type']) + )); + } + + /** + * @return string|mixed[] + */ + private function visitSimpleType() + { + $value = $this->lexer->token['value']; + + return ['name' => $value, 'params' => []]; + } + + private function visitCompoundType(): array + { + $this->root = false; + $name = $this->lexer->token['value']; + $this->match(Lexer::T_TYPE_START); + + $params = []; + if (!$this->lexer->isNextToken(Lexer::T_TYPE_END)) { + while (true) { + $params[] = $this->visit(); + + if ($this->lexer->isNextToken(Lexer::T_TYPE_END)) { + break; + } + + $this->match(Lexer::T_COMMA); + } + } + + $this->match(Lexer::T_TYPE_END); + + return [ + 'name' => $name, + 'params' => $params, + ]; } - public function parse(string $type): array + private function visitArrayType(): array { - try { - $ast = $this->parser->parse($type, 'type'); + /* + * Here we should call $this->match(Lexer::T_ARRAY_START); to make it clean + * but the token has already been consumed by moveNext() in visit() + */ + + $params = []; + if (!$this->lexer->isNextToken(Lexer::T_ARRAY_END)) { + while (true) { + $params[] = $this->visit(); + if ($this->lexer->isNextToken(Lexer::T_ARRAY_END)) { + break; + } - return $this->visitor->visit($ast); - } catch (Exception $e) { - throw new SyntaxError($e->getMessage(), 0, $e); + $this->match(Lexer::T_COMMA); + } } + + $this->match(Lexer::T_ARRAY_END); + + return $params; + } + + private function match(int $token): void + { + if (!$this->lexer->lookahead) { + throw new SyntaxError( + sprintf('Syntax error, unexpected end of stream, expected %s', $this->getConstant($token)) + ); + } + + if ($this->lexer->lookahead['type'] === $token) { + $this->lexer->moveNext(); + + return; + } + + throw new SyntaxError(sprintf( + 'Syntax error, unexpected "%s" (%s), expected was %s', + $this->lexer->lookahead['value'], + $this->getConstant($this->lexer->lookahead['type']), + $this->getConstant($token) + )); + } + + private function getConstant(int $value): string + { + $oClass = new \ReflectionClass(Lexer::class); + + return array_search($value, $oClass->getConstants()); } } diff --git a/src/Type/TypeVisitor.php b/src/Type/TypeVisitor.php deleted file mode 100644 index 6b9257c62..000000000 --- a/src/Type/TypeVisitor.php +++ /dev/null @@ -1,79 +0,0 @@ -getId()) { - case '#simple_type': - return $this->visitSimpleType($element); - case '#compound_type': - return $this->visitCompoundType($element, $handle, $eldnah); - } - - throw new InvalidNode(); - } - - /** - * @return string|mixed[] - */ - private function visitSimpleType(TreeNode $element) - { - $tokenNode = $element->getChild(0); - $token = $tokenNode->getValueToken(); - $value = $tokenNode->getValueValue(); - - if ('name' === $token) { - return ['name' => $value, 'params' => []]; - } - - if ('empty_string' === $token) { - return ''; - } - - if ('null' === $token) { - return null; - } - - if ('number' === $token) { - return false === strpos($value, '.') ? intval($value) : floatval($value); - } - - $escapeChar = 'quoted_string' === $token ? '"' : "'"; - - if (false === strpos($value, $escapeChar)) { - return $value; - } - - return str_replace($escapeChar . $escapeChar, $escapeChar, $value); - } - - private function visitCompoundType(TreeNode $element, ?int &$handle, ?int $eldnah): array - { - $nameToken = $element->getChild(0); - $parameters = array_slice($element->getChildren(), 1); - - return [ - 'name' => $nameToken->getValueValue(), - 'params' => array_map( - function (TreeNode $node) use ($handle, $eldnah) { - return $node->accept($this, $handle, $eldnah); - }, - $parameters - ), - ]; - } -} diff --git a/src/Type/grammar.pp b/src/Type/grammar.pp deleted file mode 100644 index fe98e784e..000000000 --- a/src/Type/grammar.pp +++ /dev/null @@ -1,35 +0,0 @@ -%skip whitespace \s+ - -%token parenthesis_ < -%token _parenthesis > -%token empty_string ""|'' -%token number (\+|\-)?(0|[1-9]\d*)(\.\d+)? -%token null null -%token comma , -%token name (?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\\)*[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]* - -%token quote_ " -> quoted_string -%token quoted_string:quoted_string [^"]+ -%token quoted_string:_quote " -> default - -%token apostrophe_ ' -> apostrophed_string -%token apostrophed_string:apostrophed_string [^']+ -%token apostrophed_string:_apostrophe ' -> default - -type: - simple_type() | compound_type() - -#simple_type: - - | - | - | - | ::quote_:: ::_quote:: - | ::apostrophe_:: ::_apostrophe:: - -#compound_type: - - ::parenthesis_:: - type() - ( ::comma:: type() )* - ::_parenthesis:: diff --git a/src/Type/regenerate-parser.php b/src/Type/regenerate-parser.php deleted file mode 100755 index c93c84aea..000000000 --- a/src/Type/regenerate-parser.php +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env php -options = $options; + return $this; } public function setDepth(int $depth): self { $this->depth = $depth; + return $this; } } diff --git a/src/Visitor/Factory/JsonSerializationVisitorFactory.php b/src/Visitor/Factory/JsonSerializationVisitorFactory.php index 4f2f6220a..12d3e59cb 100644 --- a/src/Visitor/Factory/JsonSerializationVisitorFactory.php +++ b/src/Visitor/Factory/JsonSerializationVisitorFactory.php @@ -24,7 +24,8 @@ public function getVisitor(): SerializationVisitorInterface public function setOptions(int $options): self { - $this->options = (int) $options; + $this->options = $options; + return $this; } } diff --git a/src/Visitor/Factory/XmlDeserializationVisitorFactory.php b/src/Visitor/Factory/XmlDeserializationVisitorFactory.php index 8938ca8bf..f0c3d2dc2 100644 --- a/src/Visitor/Factory/XmlDeserializationVisitorFactory.php +++ b/src/Visitor/Factory/XmlDeserializationVisitorFactory.php @@ -35,6 +35,7 @@ public function getVisitor(): DeserializationVisitorInterface public function enableExternalEntities(bool $enable = true): self { $this->disableExternalEntities = !$enable; + return $this; } @@ -44,12 +45,14 @@ public function enableExternalEntities(bool $enable = true): self public function setDoctypeWhitelist(array $doctypeWhitelist): self { $this->doctypeWhitelist = $doctypeWhitelist; + return $this; } public function setOptions(int $options): self { $this->options = $options; + return $this; } } diff --git a/src/Visitor/Factory/XmlSerializationVisitorFactory.php b/src/Visitor/Factory/XmlSerializationVisitorFactory.php index 0acc854fa..10541a457 100644 --- a/src/Visitor/Factory/XmlSerializationVisitorFactory.php +++ b/src/Visitor/Factory/XmlSerializationVisitorFactory.php @@ -52,24 +52,28 @@ public function setDefaultRootName(string $name, ?string $namespace = null): sel { $this->defaultRootName = $name; $this->defaultRootNamespace = $namespace; + return $this; } public function setDefaultVersion(string $version): self { $this->defaultVersion = $version; + return $this; } public function setDefaultEncoding(string $encoding): self { $this->defaultEncoding = $encoding; + return $this; } public function setFormatOutput(bool $formatOutput): self { $this->formatOutput = (bool) $formatOutput; + return $this; } } diff --git a/src/XmlDeserializationVisitor.php b/src/XmlDeserializationVisitor.php index 3b8ff1d9d..8c0a0e5b5 100644 --- a/src/XmlDeserializationVisitor.php +++ b/src/XmlDeserializationVisitor.php @@ -43,12 +43,12 @@ final class XmlDeserializationVisitor extends AbstractVisitor implements NullAwa /** * @var bool */ - private $disableExternalEntities = true; + private $disableExternalEntities; /** * @var string[] */ - private $doctypeWhitelist = []; + private $doctypeWhitelist; /** * @var int */ @@ -77,7 +77,9 @@ public function prepare($data) $previous = libxml_use_internal_errors(true); libxml_clear_errors(); - $previousEntityLoaderState = libxml_disable_entity_loader($this->disableExternalEntities); + if (\LIBXML_VERSION < 20900) { + $previousEntityLoaderState = libxml_disable_entity_loader($this->disableExternalEntities); + } if (false !== stripos($data, 'getDomDocumentTypeEntitySubset($data); @@ -92,7 +94,10 @@ public function prepare($data) $doc = simplexml_load_string($data, 'SimpleXMLElement', $this->options); libxml_use_internal_errors($previous); - libxml_disable_entity_loader($previousEntityLoaderState); + + if (\LIBXML_VERSION < 20900) { + libxml_disable_entity_loader($previousEntityLoaderState); + } if (false === $doc) { throw new XmlErrorException(libxml_get_last_error()); @@ -123,6 +128,7 @@ public function visitString($data, array $type): string { return (string) $data; } + /** * {@inheritdoc} */ @@ -138,6 +144,7 @@ public function visitBoolean($data, array $type): bool throw new RuntimeException(sprintf('Could not convert data to boolean. Expected "true", "false", "1" or "0", but got %s.', json_encode($data))); } } + /** * {@inheritdoc} */ @@ -145,6 +152,7 @@ public function visitInteger($data, array $type): int { return (int) $data; } + /** * {@inheritdoc} */ @@ -152,6 +160,7 @@ public function visitDouble($data, array $type): float { return (float) $data; } + /** * {@inheritdoc} */ @@ -162,6 +171,7 @@ public function visitArray($data, array $type): array if (2 !== count($type['params'])) { throw new RuntimeException('The array type must be specified as "array" for Key-Value-Pairs.'); } + $this->revertCurrentMetadata(); [$keyType, $entryType] = $type['params']; @@ -197,7 +207,7 @@ public function visitArray($data, array $type): array $nodes = $data->xpath($entryName); } - if (!\count($nodes)) { + if (null === $nodes || !\count($nodes)) { return []; } @@ -239,6 +249,7 @@ public function visitArray($data, array $type): array throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params']))); } } + /** * {@inheritdoc} */ @@ -256,6 +267,7 @@ public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): s // Check XML element with namespace for discriminatorFieldName case !$metadata->xmlDiscriminatorAttribute && null !== $metadata->xmlDiscriminatorNamespace && isset($data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}): return (string) $data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}; + // Check XML element for discriminatorFieldName case isset($data->{$metadata->discriminatorFieldName}): return (string) $data->{$metadata->discriminatorFieldName}; @@ -274,6 +286,7 @@ public function startVisitingObject(ClassMetadata $metadata, object $object, arr $this->setCurrentObject($object); $this->objectMetadataStack->push($metadata); } + /** * {@inheritdoc} */ @@ -285,14 +298,17 @@ public function visitProperty(PropertyMetadata $metadata, $data) if (!$metadata->type) { throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name); } + return $this->navigator->accept($data, $metadata->type); } + if ($metadata->xmlAttribute) { $attributes = $data->attributes($metadata->xmlNamespace); if (isset($attributes[$name])) { if (!$metadata->type) { throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name); } + return $this->navigator->accept($attributes[$name], $metadata->type); } @@ -303,6 +319,7 @@ public function visitProperty(PropertyMetadata $metadata, $data) if (!$metadata->type) { throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name); } + return $this->navigator->accept($data, $metadata->type); } @@ -316,8 +333,10 @@ public function visitProperty(PropertyMetadata $metadata, $data) if (!$metadata->type) { throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name); } + $v = $this->navigator->accept($enclosingElem, $metadata->type); $this->revertCurrentMetadata(); + return $v; } @@ -333,6 +352,7 @@ public function visitProperty(PropertyMetadata $metadata, $data) if (empty($nodes)) { throw new NotAcceptableException(); } + $node = reset($nodes); } else { $namespaces = $data->getDocNamespaces(); @@ -343,9 +363,11 @@ public function visitProperty(PropertyMetadata $metadata, $data) } else { $nodes = $data->xpath('./' . $name); } + if (empty($nodes)) { throw new NotAcceptableException(); } + $node = reset($nodes); } @@ -356,6 +378,7 @@ public function visitProperty(PropertyMetadata $metadata, $data) if (!$metadata->type) { throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name); } + return $this->navigator->accept($node, $metadata->type); } @@ -429,6 +452,7 @@ private function getDomDocumentTypeEntitySubset(string $data): string if ('<' === $char) { ++$braces; } + if ('>' === $char) { --$braces; } @@ -457,7 +481,8 @@ public function isNull($value): bool } $xsiAttributes = $value->attributes('http://www.w3.org/2001/XMLSchema-instance'); - if (isset($xsiAttributes['nil']) + if ( + isset($xsiAttributes['nil']) && ('true' === (string) $xsiAttributes['nil'] || '1' === (string) $xsiAttributes['nil']) ) { return true; diff --git a/src/XmlSerializationVisitor.php b/src/XmlSerializationVisitor.php index c8e342d47..48439eb67 100644 --- a/src/XmlSerializationVisitor.php +++ b/src/XmlSerializationVisitor.php @@ -25,7 +25,7 @@ final class XmlSerializationVisitor extends AbstractVisitor implements Serializa /** * @var string */ - private $defaultRootName = 'result'; + private $defaultRootName; /** * @var string|null @@ -116,15 +116,17 @@ public function createRoot(?ClassMetadata $metadata = null, ?string $rootName = $document = $this->getDocument(); if ($rootNamespace) { - $rootNode = $document->createElementNS($rootNamespace, (null !== $rootPrefix ? ($rootPrefix . ':') : '') . $rootName); + $rootNode = $document->createElementNS($rootNamespace, (null !== $rootPrefix ? $rootPrefix . ':' : '') . $rootName); } else { $rootNode = $document->createElement($rootName); } + $document->appendChild($rootNode); $this->setCurrentNode($rootNode); return $rootNode; } + /** * {@inheritdoc} */ @@ -136,6 +138,7 @@ public function visitNull($data, array $type) return $node; } + /** * {@inheritdoc} */ @@ -251,7 +254,8 @@ public function visitProperty(PropertyMetadata $metadata, $v): void return; } - if (($metadata->xmlValue && $this->currentNode->childNodes->length > 0) + if ( + ($metadata->xmlValue && $this->currentNode->childNodes->length > 0) || (!$metadata->xmlValue && $this->hasValue) ) { throw new RuntimeException(sprintf('If you make use of @XmlValue, all other properties in the class must have the @XmlAttribute annotation. Invalid usage detected in class %s.', $metadata->class)); @@ -314,6 +318,7 @@ public function visitProperty(PropertyMetadata $metadata, $v): void $this->revertCurrentMetadata(); $this->revertCurrentNode(); $this->hasValue = false; + return; } @@ -378,6 +383,7 @@ public function getResult($node) 'http://www.w3.org/2001/XMLSchema-instance' ); } + return $this->document->saveXML(); } @@ -396,6 +402,7 @@ public function getDocument(): \DOMDocument if (null === $this->document) { $this->document = $this->createDocument(); } + return $this->document; } @@ -457,6 +464,7 @@ private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $el } elseif ($element->namespaceURI === $uri) { continue; } + $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri); } } @@ -470,17 +478,22 @@ private function createElement(string $tagName, ?string $namespace = null): \DOM if ($this->parentHasNonEmptyDefaultNs()) { return $this->document->createElementNS($namespace, $tagName); } + return $this->document->createElement($tagName); } + if (null === $namespace) { return $this->document->createElement($tagName); } + if ($this->currentNode->isDefaultNamespace($namespace)) { return $this->document->createElementNS($namespace, $tagName); } + if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) { $prefix = 'ns-' . substr(sha1($namespace), 0, 8); } + return $this->document->createElementNS($namespace, $prefix . ':' . $tagName); } @@ -490,6 +503,7 @@ private function setAttributeOnNode(\DOMElement $node, string $name, string $val if (!$prefix = $node->lookupPrefix($namespace)) { $prefix = 'ns-' . substr(sha1($namespace), 0, 8); } + $node->setAttributeNS($namespace, $prefix . ':' . $name, $value); } else { $node->setAttribute($name, $value); diff --git a/tests/Exclusion/GroupsExclusionStrategyTest.php b/tests/Exclusion/GroupsExclusionStrategyTest.php index e687586d5..3352c9cc5 100644 --- a/tests/Exclusion/GroupsExclusionStrategyTest.php +++ b/tests/Exclusion/GroupsExclusionStrategyTest.php @@ -68,7 +68,7 @@ public function testGroupsFor(array $groups, array $propsVisited, array $resulti } $groupsFor = $exclusion->getGroupsFor($context); - $this->assertEquals($groupsFor, $resultingGroups); + self::assertEquals($groupsFor, $resultingGroups); } public function getGroupsFor() diff --git a/tests/Expression/ExpressionEvaluatorTest.php b/tests/Expression/ExpressionEvaluatorTest.php index ef9f07160..8af379c6c 100644 --- a/tests/Expression/ExpressionEvaluatorTest.php +++ b/tests/Expression/ExpressionEvaluatorTest.php @@ -28,7 +28,7 @@ public function testEvaluate() public function testParse() { - $parsed = $this->evaluator->parse('a + b', ['a', 'b' ]); + $parsed = $this->evaluator->parse('a + b', ['a', 'b']); self::assertInstanceOf(Expression::class, $parsed); $evaluated = $this->evaluator->evaluateParsed($parsed, ['a' => 1, 'b' => 2]); diff --git a/tests/Fixtures/AccessorSetter.php b/tests/Fixtures/AccessorSetter.php index 55731558b..57bb20513 100644 --- a/tests/Fixtures/AccessorSetter.php +++ b/tests/Fixtures/AccessorSetter.php @@ -33,9 +33,6 @@ public function getElement() return $this->element; } - /** - * @param AccessorSetterElement $element - */ public function setElementDifferent(AccessorSetterElement $element) { $this->element = new \stdClass(); diff --git a/tests/Fixtures/AuthorList.php b/tests/Fixtures/AuthorList.php index a0cec07de..396ed5bb5 100644 --- a/tests/Fixtures/AuthorList.php +++ b/tests/Fixtures/AuthorList.php @@ -18,9 +18,6 @@ class AuthorList implements \IteratorAggregate, \Countable, \ArrayAccess */ protected $authors = []; - /** - * @param Author $author - */ public function add(Author $author) { $this->authors[] = $author; diff --git a/tests/Fixtures/AuthorsInline.php b/tests/Fixtures/AuthorsInline.php index 4eab24edd..14afeb756 100644 --- a/tests/Fixtures/AuthorsInline.php +++ b/tests/Fixtures/AuthorsInline.php @@ -14,7 +14,7 @@ class AuthorsInline */ private $collection; - public function __construct(Author ... $authors) + public function __construct(Author ...$authors) { $this->collection = $authors; } diff --git a/tests/Fixtures/CircularReferenceParent.php b/tests/Fixtures/CircularReferenceParent.php index f94bab8ee..cd9a6e934 100644 --- a/tests/Fixtures/CircularReferenceParent.php +++ b/tests/Fixtures/CircularReferenceParent.php @@ -33,6 +33,7 @@ private function afterDeserialization() if (!$this->collection) { $this->collection = []; } + foreach ($this->collection as $v) { $v->setParent($this); } @@ -40,6 +41,7 @@ private function afterDeserialization() if (!$this->anotherCollection) { $this->anotherCollection = new ArrayCollection(); } + foreach ($this->anotherCollection as $v) { $v->setParent($this); } diff --git a/tests/Fixtures/DateTimeArraysObject.php b/tests/Fixtures/DateTimeArraysObject.php index 4a07543fe..a8e2afd04 100644 --- a/tests/Fixtures/DateTimeArraysObject.php +++ b/tests/Fixtures/DateTimeArraysObject.php @@ -16,7 +16,7 @@ class DateTimeArraysObject /** * @var \DateTime[] - * @Type("array>") + * @Type("array>") */ private $arrayWithFormattedDateTime; diff --git a/tests/Fixtures/Discriminator/Serialization/Entity.php b/tests/Fixtures/Discriminator/Serialization/Entity.php new file mode 100644 index 000000000..aee79bea0 --- /dev/null +++ b/tests/Fixtures/Discriminator/Serialization/Entity.php @@ -0,0 +1,38 @@ +id = $id; + } + + /** + * @throws ReflectionException + * + * @JMS\VirtualProperty() + * @JMS\SerializedName("entityName") + * @JMS\Groups({"entity.identification"}) + */ + public function getEntityName(): string + { + $reflect = new ReflectionClass($this); + + return $reflect->getShortName(); + } +} diff --git a/tests/Fixtures/Discriminator/Serialization/ExtendedUser.php b/tests/Fixtures/Discriminator/Serialization/ExtendedUser.php new file mode 100644 index 000000000..344a28094 --- /dev/null +++ b/tests/Fixtures/Discriminator/Serialization/ExtendedUser.php @@ -0,0 +1,23 @@ +extendAttribute = $extendAttribute; + } +} diff --git a/tests/Fixtures/Discriminator/Serialization/User.php b/tests/Fixtures/Discriminator/Serialization/User.php new file mode 100644 index 000000000..6ed8fb031 --- /dev/null +++ b/tests/Fixtures/Discriminator/Serialization/User.php @@ -0,0 +1,38 @@ +name = $name; + $this->description = $description; + } +} diff --git a/tests/Fixtures/DocBlockType/Collection/CollectionOfClassesFromDifferentNamespace.php b/tests/Fixtures/DocBlockType/Collection/CollectionOfClassesFromDifferentNamespace.php new file mode 100644 index 000000000..ebb737285 --- /dev/null +++ b/tests/Fixtures/DocBlockType/Collection/CollectionOfClassesFromDifferentNamespace.php @@ -0,0 +1,15 @@ + + */ + public array $productIds; +} diff --git a/tests/Fixtures/DocBlockType/Collection/Details/ProductColor.php b/tests/Fixtures/DocBlockType/Collection/Details/ProductColor.php new file mode 100644 index 000000000..787f1faf2 --- /dev/null +++ b/tests/Fixtures/DocBlockType/Collection/Details/ProductColor.php @@ -0,0 +1,9 @@ + + */ + public array $productIds; +} diff --git a/tests/Fixtures/DocBlockType/Collection/Product.php b/tests/Fixtures/DocBlockType/Collection/Product.php new file mode 100644 index 000000000..265489a37 --- /dev/null +++ b/tests/Fixtures/DocBlockType/Collection/Product.php @@ -0,0 +1,10 @@ +name = $name; + $this->id = $id; + } + + public function getName() + { + return $this->name; + } +} diff --git a/tests/Fixtures/ObjectWithEmptyNullableAndEmptyArrays.php b/tests/Fixtures/ObjectWithEmptyNullableAndEmptyArrays.php index 2ef51ef99..5be6d83e2 100644 --- a/tests/Fixtures/ObjectWithEmptyNullableAndEmptyArrays.php +++ b/tests/Fixtures/ObjectWithEmptyNullableAndEmptyArrays.php @@ -12,53 +12,53 @@ class ObjectWithEmptyNullableAndEmptyArrays * @Serializer\XmlList(inline = true, entry = "comment") * @Serializer\Type("array") */ - public $null_inline = null; + public $nullInline = null; /** * @Serializer\XmlList(inline = true, entry = "comment") * @Serializer\Type("array") */ - public $empty_inline = []; + public $emptyInline = []; /** * @Serializer\XmlList(inline = true, entry = "comment") * @Serializer\Type("array") */ - public $not_empty_inline = ['not_empty_inline']; + public $notEmptyInline = ['not_empty_inline']; /** * @Serializer\XmlList(inline = false, entry = "comment") * @Serializer\Type("array") */ - public $null_not_inline = null; + public $nullNotInline = null; /** * @Serializer\XmlList(inline = false, entry = "comment") * @Serializer\Type("array") */ - public $empty_not_inline = []; + public $emptyNotInline = []; /** * @Serializer\XmlList(inline = false, entry = "comment", skipWhenEmpty=false) * @Serializer\Type("array") */ - public $not_empty_not_inline = ['not_empty_not_inline']; + public $notEmptyNotInline = ['not_empty_not_inline']; /** * @Serializer\XmlList(inline = false, entry = "comment", skipWhenEmpty=false) * @Serializer\Type("array") */ - public $null_not_inline_skip = null; + public $nullNotInlineSkip = null; /** * @Serializer\XmlList(inline = false, entry = "comment", skipWhenEmpty=false) * @Serializer\Type("array") */ - public $empty_not_inline_skip = []; + public $emptyNotInlineSkip = []; /** * @Serializer\XmlList(inline = false, entry = "comment", skipWhenEmpty=false) * @Serializer\Type("array") */ - public $not_empty_not_inline_skip = ['not_empty_not_inline_skip']; + public $notEmptyNotInlineSkip = ['not_empty_not_inline_skip']; } diff --git a/tests/Fixtures/ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll.php b/tests/Fixtures/ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll.php new file mode 100644 index 000000000..1bbb99183 --- /dev/null +++ b/tests/Fixtures/ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll.php @@ -0,0 +1,26 @@ +isInitialized) { + $this->camelCase = 'proxy-boo'; + $this->isInitialized = true; + } + } + + public function __isInitialized() + { + return $this->isInitialized; + } + + /** + * {@inheritDoc} + */ + public function setProxyInitializer(?Closure $initializer = null) + { + $this->initializer = $initializer; + } + + public function getProxyInitializer(): ?Closure + { + return $this->initializer; + } + + public function initializeProxy(): bool + { + if (!$this->isInitialized) { + $this->camelCase = 'proxy-boo'; + $this->isInitialized = true; + + return !$this->initializer || call_user_func($this->initializer); + } + + return true; + } + + public function isProxyInitialized(): bool + { + return $this->isInitialized; + } +} diff --git a/tests/Fixtures/SimpleObjectProxy.php b/tests/Fixtures/SimpleObjectProxy.php index ddb3ddb3c..721096a06 100644 --- a/tests/Fixtures/SimpleObjectProxy.php +++ b/tests/Fixtures/SimpleObjectProxy.php @@ -4,24 +4,24 @@ namespace JMS\Serializer\Tests\Fixtures; -use Doctrine\Common\Persistence\Proxy; +use Doctrine\Persistence\Proxy; class SimpleObjectProxy extends SimpleObject implements Proxy { - public $__isInitialized__ = false; + private $isInitialized = false; private $baz = 'baz'; public function __load() { - if (!$this->__isInitialized__) { + if (!$this->isInitialized) { $this->camelCase = 'proxy-boo'; - $this->__isInitialized__ = true; + $this->isInitialized = true; } } public function __isInitialized() { - return $this->__isInitialized__; + return $this->isInitialized; } } diff --git a/tests/Fixtures/SimpleObjectWithStaticProp.php b/tests/Fixtures/SimpleObjectWithStaticProp.php index 585b4fa58..2d69a7052 100644 --- a/tests/Fixtures/SimpleObjectWithStaticProp.php +++ b/tests/Fixtures/SimpleObjectWithStaticProp.php @@ -10,7 +10,7 @@ class SimpleObjectWithStaticProp { /** @Type("string") */ - private static $foo; + private $foo; /** * @SerializedName("moo") @@ -23,7 +23,7 @@ class SimpleObjectWithStaticProp public function __construct($foo, $bar) { - self::$foo = $foo; + $this->foo = $foo; self::$bar = $bar; } } diff --git a/tests/Fixtures/TypedProperties/Car.php b/tests/Fixtures/TypedProperties/Car.php new file mode 100644 index 000000000..b8912fc36 --- /dev/null +++ b/tests/Fixtures/TypedProperties/Car.php @@ -0,0 +1,9 @@ +handler->serializeDateTime($visitor, $datetime, $type, $context); } + /** + * @param string $dateInterval + * @param \DateTime $expected + * + * @dataProvider getDeserializeDateInterval + */ + public function testDeserializeDateInterval($dateInterval, $expected) + { + $context = $this->getMockBuilder(SerializationContext::class)->getMock(); + + $visitor = $this->getMockBuilder(DeserializationVisitorInterface::class)->getMock(); + $visitor->method('visitString')->with('2017-06-18'); + + $deserialized = $this->handler->deserializeDateIntervalFromJson($visitor, $dateInterval, [], $context); + if (isset($deserialized->f)) { + $this->assertEquals($expected['f'], $deserialized->f); + } + + $this->assertEquals($expected['s'], $deserialized->s); + } + + public function getDeserializeDateInterval() + { + return [ + ['P0Y0M0DT3H5M7.520S', ['s' => 7, 'f' => 0.52]], + ['P0Y0M0DT3H5M7S', ['s' => 7, 'f' => 0]], + ]; + } + public function testTimePartGetsRemoved() { $visitor = new JsonDeserializationVisitor(); @@ -65,6 +96,17 @@ public function testTimePartGetsRemoved() ); } + public function testMultiFormatCase() + { + $visitor = new JsonDeserializationVisitor(); + + $type = ['name' => 'DateTime', 'params' => ['Y-m-d', '', ['Y-m-d|', 'Y/m/d']]]; + self::assertEquals( + \DateTime::createFromFormat('Y/m/d', '2017/06/18', $this->timezone), + $this->handler->deserializeDateTimeFromJson($visitor, '2017/06/18', $type) + ); + } + public function testTimePartGetsPreserved() { $visitor = new JsonDeserializationVisitor(); diff --git a/tests/Handler/FormErrorHandlerTest.php b/tests/Handler/FormErrorHandlerTest.php index 997e54acc..47cdfbe68 100644 --- a/tests/Handler/FormErrorHandlerTest.php +++ b/tests/Handler/FormErrorHandlerTest.php @@ -9,6 +9,7 @@ use JMS\Serializer\JsonSerializationVisitor; use JMS\Serializer\SerializationContext; use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -20,8 +21,12 @@ use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\Forms; use Symfony\Component\Translation\Translator; +use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Validation; +use Symfony\Contracts\Translation\TranslatorInterface; + +use function assert; class FormErrorHandlerTest extends TestCase { @@ -144,8 +149,11 @@ public function testSerializeChildElements() public function testDefaultTranslationDomain() { - /** @var Translator|\PHPUnit_Framework_MockObject_MockObject $translator */ - $translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); + $interface = interface_exists(TranslatorInterface::class) + ? TranslatorInterface::class + : LegacyTranslatorInterface::class; + $translator = $this->getMockBuilder($interface)->getMock(); + assert($translator instanceof Translator || $translator instanceof MockObject); $handler = new FormErrorHandler($translator); @@ -167,19 +175,32 @@ public function testDefaultTranslationDomain() public function testDefaultTranslationDomainWithPluralTranslation() { - /** @var Translator|\PHPUnit_Framework_MockObject_MockObject $translator */ - $translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); + $interface = interface_exists(TranslatorInterface::class) + ? TranslatorInterface::class + : LegacyTranslatorInterface::class; + $translator = $this->getMockBuilder($interface)->getMock(); + assert($translator instanceof Translator || $translator instanceof MockObject); $handler = new FormErrorHandler($translator); - $translator->expects($this->once()) - ->method('transChoice') - ->with( - $this->equalTo('error!'), - $this->equalTo(0), - $this->equalTo([]), - $this->equalTo('validators') - ); + if (TranslatorInterface::class === $interface) { + $translator->expects($this->once()) + ->method('trans') + ->with( + $this->equalTo('error!'), + $this->equalTo(['%count%' => 0]), + $this->equalTo('validators') + ); + } else { + $translator->expects($this->once()) + ->method('transChoice') + ->with( + $this->equalTo('error!'), + $this->equalTo(0), + $this->equalTo([]), + $this->equalTo('validators') + ); + } $formError = $this->getMockBuilder('Symfony\Component\Form\FormError')->disableOriginalConstructor()->getMock(); $formError->expects($this->once())->method('getMessageTemplate')->willReturn('error!'); @@ -191,8 +212,11 @@ public function testDefaultTranslationDomainWithPluralTranslation() public function testCustomTranslationDomain() { - /** @var Translator|\PHPUnit_Framework_MockObject_MockObject $translator */ - $translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); + $interface = interface_exists(TranslatorInterface::class) + ? TranslatorInterface::class + : LegacyTranslatorInterface::class; + $translator = $this->getMockBuilder($interface)->getMock(); + assert($translator instanceof Translator || $translator instanceof MockObject); $handler = new FormErrorHandler($translator, 'custom_domain'); @@ -214,19 +238,33 @@ public function testCustomTranslationDomain() public function testCustomTranslationDomainWithPluralTranslation() { - /** @var Translator|\PHPUnit_Framework_MockObject_MockObject $translator */ $translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); + assert($translator instanceof Translator || $translator instanceof MockObject); + $interface = interface_exists(TranslatorInterface::class) + ? TranslatorInterface::class + : LegacyTranslatorInterface::class; + $translator = $this->getMockBuilder($interface)->getMock(); $handler = new FormErrorHandler($translator, 'custom_domain'); - $translator->expects($this->once()) - ->method('transChoice') - ->with( - $this->equalTo('error!'), - $this->equalTo(0), - $this->equalTo([]), - $this->equalTo('custom_domain') - ); + if (TranslatorInterface::class === $interface) { + $translator->expects($this->once()) + ->method('trans') + ->with( + $this->equalTo('error!'), + $this->equalTo(['%count%' => 0]), + $this->equalTo('custom_domain') + ); + } else { + $translator->expects($this->once()) + ->method('transChoice') + ->with( + $this->equalTo('error!'), + $this->equalTo(0), + $this->equalTo([]), + $this->equalTo('custom_domain') + ); + } $formError = $this->getMockBuilder('Symfony\Component\Form\FormError')->disableOriginalConstructor()->getMock(); $formError->expects($this->once())->method('getMessageTemplate')->willReturn('error!'); @@ -250,10 +288,8 @@ protected function getBuilder($name = 'name', ?EventDispatcherInterface $dispatc /** * @param string $name - * - * @return \PHPUnit_Framework_MockObject_MockObject */ - protected function getMockForm($name = 'name') + protected function getMockForm($name = 'name'): MockObject { $form = $this->getMockBuilder('Symfony\Component\Form\Test\FormInterface')->getMock(); $config = $this->getMockBuilder('Symfony\Component\Form\FormConfigInterface')->getMock(); diff --git a/tests/Handler/IteratorHandlerTest.php b/tests/Handler/IteratorHandlerTest.php index fb4f131ac..1c9169133 100644 --- a/tests/Handler/IteratorHandlerTest.php +++ b/tests/Handler/IteratorHandlerTest.php @@ -84,6 +84,7 @@ private function createDeserializationVisitor(): DeserializationVisitorInterface { $visitor = $this->getMockBuilder(DeserializationVisitorInterface::class)->getMock(); $visitor->method('visitArray')->with(self::DATA)->willReturn(self::DATA); + return $visitor; } @@ -91,6 +92,7 @@ private function createSerializationVisitor(): SerializationVisitorInterface { $visitor = $this->getMockBuilder(SerializationVisitorInterface::class)->getMock(); $visitor->method('visitArray')->with(self::DATA)->willReturn(self::DATA); + return $visitor; } } diff --git a/tests/Metadata/Driver/AnnotationDriverTest.php b/tests/Metadata/Driver/AnnotationDriverTest.php index e84e66228..4afb55bc5 100644 --- a/tests/Metadata/Driver/AnnotationDriverTest.php +++ b/tests/Metadata/Driver/AnnotationDriverTest.php @@ -8,6 +8,7 @@ use JMS\Serializer\Metadata\Driver\AnnotationDriver; use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy; use JMS\Serializer\Tests\Fixtures\AllExcludedObject; +use Metadata\Driver\DriverInterface; class AnnotationDriverTest extends BaseDriverTest { @@ -20,7 +21,7 @@ public function testAllExcluded() self::assertArrayHasKey('bar', $m->propertyMetadata); } - protected function getDriver() + protected function getDriver(?string $subDir = null, bool $addUnderscoreDir = true): DriverInterface { return new AnnotationDriver(new AnnotationReader(), new IdenticalPropertyNamingStrategy(), null, $this->getExpressionEvaluator()); } @@ -29,4 +30,9 @@ public function testCanDefineMetadataForInternalClass() { $this->markTestSkipped('Can not define annotation metadata for internal classes'); } + + public function testShortExposeSyntax(): void + { + $this->markTestSkipped('Short expose syntax not supported on annotations'); + } } diff --git a/tests/Metadata/Driver/BaseDriverTest.php b/tests/Metadata/Driver/BaseDriverTest.php index bf3e64b0e..42376c910 100644 --- a/tests/Metadata/Driver/BaseDriverTest.php +++ b/tests/Metadata/Driver/BaseDriverTest.php @@ -21,8 +21,10 @@ use JMS\Serializer\Tests\Fixtures\ObjectWithExpressionVirtualPropertiesAndExcludeAll; use JMS\Serializer\Tests\Fixtures\ObjectWithInvalidExpression; use JMS\Serializer\Tests\Fixtures\ObjectWithVirtualPropertiesAndDuplicatePropName; +use JMS\Serializer\Tests\Fixtures\ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll; use JMS\Serializer\Tests\Fixtures\ObjectWithVirtualPropertiesAndExcludeAll; use JMS\Serializer\Tests\Fixtures\ParentSkipWithEmptyChild; +use JMS\Serializer\Tests\Fixtures\Person; use Metadata\Driver\DriverInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\ExpressionLanguage\ExpressionFunction; @@ -207,8 +209,8 @@ public function testReadOnlyDefinedBeforeGetterAndSetter() public function testExpressionVirtualProperty() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\AuthorExpressionAccess')); + \assert($m instanceof ClassMetadata); $keys = array_keys($m->propertyMetadata); self::assertEquals(['firstName', 'lastName', 'id'], $keys); @@ -216,8 +218,8 @@ public function testExpressionVirtualProperty() public function testLoadDiscriminator() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\Discriminator\Vehicle')); + \assert($m instanceof ClassMetadata); self::assertNotNull($m); self::assertEquals('type', $m->discriminatorFieldName); @@ -233,8 +235,8 @@ public function testLoadDiscriminator() public function testLoadDiscriminatorWhenParentIsInDiscriminatorMap() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\Discriminator\Post')); + \assert($m instanceof ClassMetadata); self::assertNotNull($m); self::assertEquals('type', $m->discriminatorFieldName); @@ -250,8 +252,8 @@ public function testLoadDiscriminatorWhenParentIsInDiscriminatorMap() public function testLoadXmlDiscriminator() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass(ObjectWithXmlAttributeDiscriminatorParent::class)); + \assert($m instanceof ClassMetadata); self::assertNotNull($m); self::assertEquals('type', $m->discriminatorFieldName); @@ -268,8 +270,8 @@ public function testLoadXmlDiscriminator() public function testLoadXmlDiscriminatorWithNamespaces() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass(ObjectWithXmlNamespaceDiscriminatorParent::class)); + \assert($m instanceof ClassMetadata); self::assertNotNull($m); self::assertEquals('type', $m->discriminatorFieldName); @@ -286,8 +288,8 @@ public function testLoadXmlDiscriminatorWithNamespaces() public function testCanDefineMetadataForInternalClass() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass(\PDOStatement::class)); + \assert($m instanceof ClassMetadata); self::assertNotNull($m); self::assertSame('int', $m->propertyMetadata['queryString']->type['name']); @@ -297,8 +299,8 @@ public function testCanDefineMetadataForInternalClass() public function testLoadXmlDiscriminatorWithAttributeNamespaces() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass(ObjectWithXmlNamespaceAttributeDiscriminatorParent::class)); + \assert($m instanceof ClassMetadata); self::assertNotNull($m); self::assertEquals('type', $m->discriminatorFieldName); @@ -315,8 +317,8 @@ public function testLoadXmlDiscriminatorWithAttributeNamespaces() public function testLoadDiscriminatorWithGroup() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\DiscriminatorGroup\Vehicle')); + \assert($m instanceof ClassMetadata); self::assertNotNull($m); self::assertEquals('type', $m->discriminatorFieldName); @@ -330,8 +332,8 @@ public function testLoadDiscriminatorWithGroup() public function testSkipWhenEmptyOption() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass(ParentSkipWithEmptyChild::class)); + \assert($m instanceof ClassMetadata); self::assertNotNull($m); @@ -345,8 +347,8 @@ public function testSkipWhenEmptyOption() public function testLoadDiscriminatorSubClass() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\Discriminator\Car')); + \assert($m instanceof ClassMetadata); self::assertNotNull($m); self::assertNull($m->discriminatorValue); @@ -357,8 +359,8 @@ public function testLoadDiscriminatorSubClass() public function testLoadDiscriminatorSubClassWhenParentIsInDiscriminatorMap() { - /** @var ClassMetadata $m */ $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\Discriminator\ImagePost')); + \assert($m instanceof ClassMetadata); self::assertNotNull($m); self::assertNull($m->discriminatorValue); @@ -566,6 +568,28 @@ public function testExclusionIf() self::assertEquals($p, $m->propertyMetadata['age']); } + public function testExclusionIfOnClass() + { + $class = 'JMS\Serializer\Tests\Fixtures\PersonAccount'; + $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass($class)); + + $c = new ClassMetadata($class); + $c->excludeIf = $this->getExpressionEvaluator()->parse('object.expired', ['context', 'class_metadata', 'object']); + self::assertEquals($c->excludeIf->serialize(), $m->excludeIf->serialize()); + } + + public function testObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll() + { + $class = ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll::class; + $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass($class)); + + $p = new VirtualPropertyMetadata($class, 'name'); + $p->serializedName = 'mood'; + $p->getter = 'getName'; + + self::assertEquals($p, $m->propertyMetadata['name']); + } + public function testObjectWithVirtualPropertiesAndDuplicatePropName() { $class = ObjectWithVirtualPropertiesAndDuplicatePropName::class; @@ -599,14 +623,20 @@ public function testExcludePropertyNoPublicAccessorException() if ($this instanceof PhpDriverTest) { return; } + self::assertArrayHasKey('id', $first->propertyMetadata); self::assertArrayNotHasKey('iShallNotBeAccessed', $first->propertyMetadata); } - /** - * @return DriverInterface - */ - abstract protected function getDriver(); + public function testShortExposeSyntax(): void + { + $m = $this->getDriver('short_expose')->loadMetadataForClass(new \ReflectionClass(Person::class)); + + self::assertArrayHasKey('name', $m->propertyMetadata); + self::assertArrayNotHasKey('age', $m->propertyMetadata); + } + + abstract protected function getDriver(?string $subDir = null, bool $addUnderscoreDir = true): DriverInterface; protected function getExpressionEvaluator() { @@ -617,6 +647,7 @@ protected function getExpressionEvaluator() }, static function () { return true; })); + return new ExpressionEvaluator($language); } } diff --git a/tests/Metadata/Driver/DefaultDriverFactoryTest.php b/tests/Metadata/Driver/DefaultDriverFactoryTest.php new file mode 100644 index 000000000..59112b321 --- /dev/null +++ b/tests/Metadata/Driver/DefaultDriverFactoryTest.php @@ -0,0 +1,39 @@ +markTestSkipped(sprintf('%s requires PHP 7.4', __METHOD__)); + } + + $factory = new DefaultDriverFactory(new IdenticalPropertyNamingStrategy()); + + $driver = $factory->createDriver([], new AnnotationReader()); + + $m = $driver->loadMetadataForClass(new \ReflectionClass(User::class)); + self::assertNotNull($m); + + $expectedPropertyTypes = [ + 'id' => 'int', + 'role' => 'JMS\Serializer\Tests\Fixtures\TypedProperties\Role', + 'created' => 'DateTime', + 'tags' => 'iterable', + ]; + + foreach ($expectedPropertyTypes as $property => $type) { + self::assertEquals(['name' => $type, 'params' => []], $m->propertyMetadata[$property]->type); + } + } +} diff --git a/tests/Metadata/Driver/DocBlockDriverTest.php b/tests/Metadata/Driver/DocBlockDriverTest.php new file mode 100644 index 000000000..cac5a78d3 --- /dev/null +++ b/tests/Metadata/Driver/DocBlockDriverTest.php @@ -0,0 +1,346 @@ + 70400) { + $baseDriver = new TypedPropertiesDriver(new AnnotationDriver(new AnnotationReader(), new IdenticalPropertyNamingStrategy())); + } else { + $baseDriver = new AnnotationDriver(new AnnotationReader(), new IdenticalPropertyNamingStrategy()); + } + + $driver = new DocBlockDriver($baseDriver); + + $m = $driver->loadMetadataForClass(new \ReflectionClass($classToResolve)); + self::assertNotNull($m); + + return $m; + } + + public function testInferDocBlockCollectionOfScalars() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfScalars::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => 'string', 'params' => []]]], + $m->propertyMetadata['productIds']->type + ); + } + + public function testInferDocBlockCollectionOfClassesFromSameNamespace() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfClassesFromSameNamespace::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => Product::class, 'params' => []]]], + $m->propertyMetadata['productIds']->type + ); + } + + public function testInferDocBlockCollectionOfClassesFromUsingFullNamespacePath() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfClassesWithFullNamespacePath::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => Product::class, 'params' => []]]], + $m->propertyMetadata['productIds']->type + ); + } + + public function testInferDocBlockCollectionFromGenericLikeClass() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionTypedAsGenericClass::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => Product::class, 'params' => []]]], + $m->propertyMetadata['productIds']->type + ); + } + + public function testInferDocBlockMapFromGenericLikeClass() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(MapTypedAsGenericClass::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => 'int', 'params' => []], ['name' => Product::class, 'params' => []]]], + $m->propertyMetadata['productIds']->type + ); + } + + public function testInferDocBlockCollectionOfClassesIgnoringNullTypeHint() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfClassesWithNull::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => Product::class, 'params' => []]]], + $m->propertyMetadata['productIds']->type + ); + } + + public function testInferDocBlockCollectionOfClassesIgnoringNullTypeHintWithSingleLinePhpDoc() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfClassesWithNullSingleLinePhpDoc::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => Product::class, 'params' => []]]], + $m->propertyMetadata['productIds']->type + ); + } + + public function testThrowingExceptionWhenUnionTypeIsUsedForCollection() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $this->expectException(\InvalidArgumentException::class); + + $this->resolve(CollectionOfUnionClasses::class); + } + + public function testThrowingExceptionWhenNotExistingClassWasGiven() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $this->expectException(\InvalidArgumentException::class); + + $this->resolve(CollectionOfNotExistingClasses::class); + } + + public function testInferDocBlockCollectionOfClassesFromDifferentNamespace() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfClassesFromDifferentNamespace::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => ProductDescription::class, 'params' => []]]], + $m->propertyMetadata['productDescriptions']->type + ); + } + + public function testInferDocBlockCollectionOfClassesFromGlobalNamespace() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfClassesFromGlobalNamespace::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => \stdClass::class, 'params' => []]]], + $m->propertyMetadata['products']->type + ); + } + + public function testInferDocBlockCollectionOfClassesFromDifferentNamespaceUsingSingleAlias() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfClassesFromDifferentNamespaceUsingSingleAlias::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => ProductDescription::class, 'params' => []]]], + $m->propertyMetadata['productDescriptions']->type + ); + } + + public function testInferDocBlockCollectionOfClassesFromDifferentNamespaceUsingGroupAlias() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfClassesFromDifferentNamespaceUsingGroupAlias::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => ProductDescription::class, 'params' => []]]], + $m->propertyMetadata['productDescriptions']->type + ); + self::assertEquals( + ['name' => 'array', 'params' => [['name' => ProductName::class, 'params' => []]]], + $m->propertyMetadata['productNames']->type + ); + } + + public function testInferDocBlockCollectionOfClassesFromTraits() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfClassesFromTrait::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => ProductDescription::class, 'params' => []]]], + $m->propertyMetadata['productDescriptions']->type + ); + self::assertEquals( + ['name' => 'array', 'params' => [['name' => ProductName::class, 'params' => []]]], + $m->propertyMetadata['productNames']->type + ); + } + + public function testInferDocBlockCollectionOfClassesFromTraitInsideTrait() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfClassesFromTraitInsideTrait::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => ProductDescription::class, 'params' => []]]], + $m->propertyMetadata['productDescriptions']->type + ); + } + + public function testInferDocBlockCollectionOfInterfacesFromDifferentNamespace() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfInterfacesFromDifferentNamespace::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => ProductColor::class, 'params' => []]]], + $m->propertyMetadata['productColors']->type + ); + } + + public function testInferDocBlockCollectionOfInterfacesFromGlobalNamespace() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfInterfacesFromGlobalNamespace::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => ProductColor::class, 'params' => []]]], + $m->propertyMetadata['productColors']->type + ); + } + + public function testInferDocBlockCollectionOfInterfacesFromSameNamespace() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfInterfacesFromSameNamespace::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => Vehicle::class, 'params' => []]]], + $m->propertyMetadata['vehicles']->type + ); + } + + public function testInferDocBlockCollectionOfInterfacesWithFullNamespacePath() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + + $m = $this->resolve(CollectionOfInterfacesWithFullNamespacePath::class); + + self::assertEquals( + ['name' => 'array', 'params' => [['name' => ProductColor::class, 'params' => []]]], + $m->propertyMetadata['productColors']->type + ); + } + + public function testInferTypeForNonCollectionFromSameNamespaceType() + { + $m = $this->resolve(SingleClassFromGlobalNamespaceTypeHint::class); + + self::assertEquals( + ['name' => \stdClass::class, 'params' => []], + $m->propertyMetadata['data']->type + ); + } + + public function testInferTypeForNonCollectionFromDifferentNamespaceType() + { + $m = $this->resolve(SingleClassFromDifferentNamespaceTypeHint::class); + + self::assertEquals( + ['name' => ProductDescription::class, 'params' => []], + $m->propertyMetadata['data']->type + ); + } +} diff --git a/tests/Metadata/Driver/DoctrineDriverTest.php b/tests/Metadata/Driver/DoctrineDriverTest.php index 525b4cfb2..602cf1d3d 100644 --- a/tests/Metadata/Driver/DoctrineDriverTest.php +++ b/tests/Metadata/Driver/DoctrineDriverTest.php @@ -9,6 +9,7 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\AnnotationDriver as DoctrineDriver; use Doctrine\ORM\Version as ORMVersion; +use Doctrine\Persistence\ManagerRegistry; use JMS\Serializer\Metadata\Driver\AnnotationDriver; use JMS\Serializer\Metadata\Driver\DoctrineTypeDriver; use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy; @@ -20,6 +21,7 @@ class DoctrineDriverTest extends TestCase public function getMetadata() { $refClass = new \ReflectionClass('JMS\Serializer\Tests\Fixtures\Doctrine\Entity\BlogPost'); + return $this->getDoctrineDriver()->loadMetadataForClass($refClass); } @@ -151,7 +153,7 @@ public function getAnnotationDriver() protected function getDoctrineDriver() { - $registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock(); + $registry = $this->getMockBuilder(ManagerRegistry::class)->getMock(); $registry->expects($this->atLeastOnce()) ->method('getManagerForClass') ->will($this->returnValue($this->getEntityManager())); diff --git a/tests/Metadata/Driver/DoctrinePHPCRDriverTest.php b/tests/Metadata/Driver/DoctrinePHPCRDriverTest.php index 6d0b35528..ea1e5980a 100644 --- a/tests/Metadata/Driver/DoctrinePHPCRDriverTest.php +++ b/tests/Metadata/Driver/DoctrinePHPCRDriverTest.php @@ -8,6 +8,7 @@ use Doctrine\ODM\PHPCR\Configuration; use Doctrine\ODM\PHPCR\DocumentManager; use Doctrine\ODM\PHPCR\Mapping\Driver\AnnotationDriver as DoctrinePHPCRDriver; +use Doctrine\Persistence\ManagerRegistry; use JMS\Serializer\Metadata\Driver\AnnotationDriver; use JMS\Serializer\Metadata\Driver\DoctrinePHPCRTypeDriver; use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy; @@ -15,9 +16,17 @@ class DoctrinePHPCRDriverTest extends TestCase { + protected function setUp(): void + { + if (!class_exists(Configuration::class)) { + $this->markTestSkipped('PHPCR not available'); + } + } + public function getMetadata() { $refClass = new \ReflectionClass('JMS\Serializer\Tests\Fixtures\DoctrinePHPCR\BlogPost'); + return $this->getDoctrinePHPCRDriver()->loadMetadataForClass($refClass); } @@ -103,7 +112,7 @@ public function getAnnotationDriver() protected function getDoctrinePHPCRDriver() { - $registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock(); + $registry = $this->getMockBuilder(ManagerRegistry::class)->getMock(); $registry->expects($this->atLeastOnce()) ->method('getManagerForClass') ->will($this->returnValue($this->getDocumentManager())); diff --git a/tests/Metadata/Driver/TypedPropertiesDriverTest.php b/tests/Metadata/Driver/TypedPropertiesDriverTest.php new file mode 100644 index 000000000..e978ad9aa --- /dev/null +++ b/tests/Metadata/Driver/TypedPropertiesDriverTest.php @@ -0,0 +1,52 @@ +resolve(User::class); + + $expectedPropertyTypes = [ + 'id' => 'int', + 'role' => 'JMS\Serializer\Tests\Fixtures\TypedProperties\Role', + 'vehicle' => 'JMS\Serializer\Tests\Fixtures\TypedProperties\Vehicle', + 'created' => 'DateTime', + 'tags' => 'iterable', + ]; + + foreach ($expectedPropertyTypes as $property => $type) { + self::assertEquals(['name' => $type, 'params' => []], $m->propertyMetadata[$property]->type); + } + } + + private function resolve(string $classToResolve): ClassMetadata + { + $baseDriver = new AnnotationDriver(new AnnotationReader(), new IdenticalPropertyNamingStrategy()); + $driver = new TypedPropertiesDriver($baseDriver); + + $m = $driver->loadMetadataForClass(new ReflectionClass($classToResolve)); + self::assertNotNull($m); + + return $m; + } + + protected function setUp(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', TypedPropertiesDriver::class)); + } + } +} diff --git a/tests/Metadata/Driver/XmlDriverTest.php b/tests/Metadata/Driver/XmlDriverTest.php index 08397da44..da1cfcbb8 100644 --- a/tests/Metadata/Driver/XmlDriverTest.php +++ b/tests/Metadata/Driver/XmlDriverTest.php @@ -4,23 +4,25 @@ namespace JMS\Serializer\Tests\Metadata\Driver; +use JMS\Serializer\Exception\InvalidMetadataException; use JMS\Serializer\Metadata\Driver\XmlDriver; use JMS\Serializer\Metadata\PropertyMetadata; use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy; +use Metadata\Driver\DriverInterface; use Metadata\Driver\FileLocator; class XmlDriverTest extends BaseDriverTest { - /** - * @expectedException \JMS\Serializer\Exception\InvalidMetadataException - * @expectedExceptionMessage Invalid XML content for metadata - */ public function testInvalidXml() { $driver = $this->getDriver(); $ref = new \ReflectionMethod($driver, 'loadMetadataFromFile'); $ref->setAccessible(true); + + $this->expectException(InvalidMetadataException::class); + $this->expectExceptionMessage('Invalid XML content for metadata'); + $ref->invoke($driver, new \ReflectionClass('stdClass'), __DIR__ . '/xml/invalid.xml'); } @@ -76,7 +78,8 @@ public function testGroupsTrim() $first = $this->getDriver()->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\GroupsTrim')); self::assertArrayHasKey('amount', $first->propertyMetadata); - self::assertArraySubset(['first.test.group', 'second.test.group'], $first->propertyMetadata['currency']->groups); + self::assertContains('first.test.group', $first->propertyMetadata['currency']->groups); + self::assertContains('second.test.group', $first->propertyMetadata['currency']->groups); } public function testMultilineGroups() @@ -84,19 +87,20 @@ public function testMultilineGroups() $first = $this->getDriver()->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\MultilineGroupsFormat')); self::assertArrayHasKey('amount', $first->propertyMetadata); - self::assertArraySubset(['first.test.group', 'second.test.group'], $first->propertyMetadata['currency']->groups); + self::assertContains('first.test.group', $first->propertyMetadata['currency']->groups); + self::assertContains('second.test.group', $first->propertyMetadata['currency']->groups); } - protected function getDriver() + protected function getDriver(?string $subDir = null, bool $addUnderscoreDir = true): DriverInterface { - $append = ''; - if (1 === func_num_args()) { - $append = '/' . func_get_arg(0); + $dirs = [ + 'JMS\Serializer\Tests\Fixtures' => __DIR__ . '/xml' . ($subDir ? '/' . $subDir : ''), + ]; + + if ($addUnderscoreDir) { + $dirs[''] = __DIR__ . '/xml/_' . ($subDir ? '/' . $subDir : ''); } - return new XmlDriver(new FileLocator([ - 'JMS\Serializer\Tests\Fixtures' => __DIR__ . '/xml' . $append, - '' => __DIR__ . '/xml/_' . $append, - ]), new IdenticalPropertyNamingStrategy(), null, $this->getExpressionEvaluator()); + return new XmlDriver(new FileLocator($dirs), new IdenticalPropertyNamingStrategy(), null, $this->getExpressionEvaluator()); } } diff --git a/tests/Metadata/Driver/YamlDriverTest.php b/tests/Metadata/Driver/YamlDriverTest.php index 41a76a414..5cde191f9 100644 --- a/tests/Metadata/Driver/YamlDriverTest.php +++ b/tests/Metadata/Driver/YamlDriverTest.php @@ -4,24 +4,26 @@ namespace JMS\Serializer\Tests\Metadata\Driver; +use JMS\Serializer\Exception\InvalidMetadataException; use JMS\Serializer\Metadata\Driver\YamlDriver; use JMS\Serializer\Metadata\PropertyMetadata; use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy; use JMS\Serializer\Tests\Fixtures\BlogPost; use JMS\Serializer\Tests\Fixtures\Person; +use Metadata\Driver\DriverInterface; use Metadata\Driver\FileLocator; class YamlDriverTest extends BaseDriverTest { public function testAccessorOrderIsInferred(): void { - $m = $this->getDriverForSubDir('accessor_inferred')->loadMetadataForClass(new \ReflectionClass(Person::class)); + $m = $this->getDriver('accessor_inferred')->loadMetadataForClass(new \ReflectionClass(Person::class)); self::assertEquals(['age', 'name'], array_keys($m->propertyMetadata)); } public function testShortExposeSyntax(): void { - $m = $this->getDriverForSubDir('short_expose')->loadMetadataForClass(new \ReflectionClass(Person::class)); + $m = $this->getDriver('short_expose')->loadMetadataForClass(new \ReflectionClass(Person::class)); self::assertArrayHasKey('name', $m->propertyMetadata); self::assertArrayNotHasKey('age', $m->propertyMetadata); @@ -29,7 +31,7 @@ public function testShortExposeSyntax(): void public function testBlogPost(): void { - $m = $this->getDriverForSubDir('exclude_all')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); + $m = $this->getDriver('exclude_all')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); self::assertArrayHasKey('title', $m->propertyMetadata); @@ -41,7 +43,7 @@ public function testBlogPost(): void public function testBlogPostExcludeNoneStrategy(): void { - $m = $this->getDriverForSubDir('exclude_none')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); + $m = $this->getDriver('exclude_none')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); self::assertArrayNotHasKey('title', $m->propertyMetadata); @@ -53,7 +55,7 @@ public function testBlogPostExcludeNoneStrategy(): void public function testBlogPostCaseInsensitive(): void { - $m = $this->getDriverForSubDir('case')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); + $m = $this->getDriver('case')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); $p = new PropertyMetadata($m->name, 'title'); $p->serializedName = 'title'; @@ -63,7 +65,7 @@ public function testBlogPostCaseInsensitive(): void public function testBlogPostAccessor(): void { - $m = $this->getDriverForSubDir('accessor')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); + $m = $this->getDriver('accessor')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); self::assertArrayHasKey('title', $m->propertyMetadata); @@ -74,24 +76,23 @@ public function testBlogPostAccessor(): void self::assertEquals($p, $m->propertyMetadata['title']); } - /** - * @expectedException \JMS\Serializer\Exception\InvalidMetadataException - */ public function testInvalidMetadataFileCausesException(): void { - $this->getDriverForSubDir('invalid_metadata')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); + $this->expectException(InvalidMetadataException::class); + + $this->getDriver('invalid_metadata')->loadMetadataForClass(new \ReflectionClass(BlogPost::class)); } public function testLoadingYamlFileWithLongExtension(): void { - $m = $this->getDriverForSubDir('multiple_types')->loadMetadataForClass(new \ReflectionClass(Person::class)); + $m = $this->getDriver('multiple_types')->loadMetadataForClass(new \ReflectionClass(Person::class)); self::assertArrayHasKey('name', $m->propertyMetadata); } public function testLoadingMultipleMetadataExtensions(): void { - $classNames = $this->getDriverForSubDir('multiple_types', false)->getAllClassNames(); + $classNames = $this->getDriver('multiple_types', false)->getAllClassNames(); self::assertEquals( [ @@ -102,7 +103,7 @@ public function testLoadingMultipleMetadataExtensions(): void ); } - private function getDriverForSubDir($subDir = null, bool $addUnderscoreDir = true): YamlDriver + protected function getDriver(?string $subDir = null, bool $addUnderscoreDir = true): DriverInterface { $dirs = [ 'JMS\Serializer\Tests\Fixtures' => __DIR__ . '/yml' . ($subDir ? '/' . $subDir : ''), @@ -114,9 +115,4 @@ private function getDriverForSubDir($subDir = null, bool $addUnderscoreDir = tru return new YamlDriver(new FileLocator($dirs), new IdenticalPropertyNamingStrategy(), null, $this->getExpressionEvaluator()); } - - protected function getDriver() - { - return $this->getDriverForSubDir(); - } } diff --git a/tests/Metadata/Driver/xml/ObjectWithExpressionVirtualPropertiesAndExcludeAll.xml b/tests/Metadata/Driver/xml/ObjectWithExpressionVirtualPropertiesAndExcludeAll.xml index 0219cbe11..3164215cf 100644 --- a/tests/Metadata/Driver/xml/ObjectWithExpressionVirtualPropertiesAndExcludeAll.xml +++ b/tests/Metadata/Driver/xml/ObjectWithExpressionVirtualPropertiesAndExcludeAll.xml @@ -1,6 +1,6 @@ - + diff --git a/tests/Metadata/Driver/xml/ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll.xml b/tests/Metadata/Driver/xml/ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll.xml new file mode 100644 index 000000000..c04373316 --- /dev/null +++ b/tests/Metadata/Driver/xml/ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/Metadata/Driver/xml/PersonAccount.xml b/tests/Metadata/Driver/xml/PersonAccount.xml new file mode 100644 index 000000000..2cba0ff8b --- /dev/null +++ b/tests/Metadata/Driver/xml/PersonAccount.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/Metadata/Driver/xml/short_expose/Person.xml b/tests/Metadata/Driver/xml/short_expose/Person.xml new file mode 100644 index 000000000..92c4d09ff --- /dev/null +++ b/tests/Metadata/Driver/xml/short_expose/Person.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/Metadata/Driver/yml/ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll.yml b/tests/Metadata/Driver/yml/ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll.yml new file mode 100644 index 000000000..5d1186288 --- /dev/null +++ b/tests/Metadata/Driver/yml/ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll.yml @@ -0,0 +1,5 @@ +JMS\Serializer\Tests\Fixtures\ObjectWithVirtualPropertiesAndDuplicatePropNameExcludeAll: + exclusion_policy: all + virtual_properties: + getName: + serialized_name: mood diff --git a/tests/Metadata/Driver/yml/PersonAccount.yaml b/tests/Metadata/Driver/yml/PersonAccount.yaml new file mode 100644 index 000000000..be4b00f38 --- /dev/null +++ b/tests/Metadata/Driver/yml/PersonAccount.yaml @@ -0,0 +1,6 @@ +JMS\Serializer\Tests\Fixtures\PersonAccount: + exclude_if: 'object.expired' + properties: + title: + name: string + expired: boolean \ No newline at end of file diff --git a/tests/Metadata/ExpressionPropertyMetadataTest.php b/tests/Metadata/ExpressionPropertyMetadataTest.php index 70160d652..1cc5abce2 100644 --- a/tests/Metadata/ExpressionPropertyMetadataTest.php +++ b/tests/Metadata/ExpressionPropertyMetadataTest.php @@ -14,6 +14,6 @@ public function testSerialization() $this->setNonDefaultMetadataValues($meta); $restoredMeta = unserialize(serialize($meta)); - $this->assertEquals($meta, $restoredMeta); + self::assertEquals($meta, $restoredMeta); } } diff --git a/tests/Metadata/PropertyMetadataTest.php b/tests/Metadata/PropertyMetadataTest.php index 24c478357..c50fe0943 100644 --- a/tests/Metadata/PropertyMetadataTest.php +++ b/tests/Metadata/PropertyMetadataTest.php @@ -18,6 +18,6 @@ public function testSerialization() $meta->readOnly = true; $restoredMeta = unserialize(serialize($meta)); - $this->assertEquals($meta, $restoredMeta); + self::assertEquals($meta, $restoredMeta); } } diff --git a/tests/Metadata/StaticPropertyMetadataTest.php b/tests/Metadata/StaticPropertyMetadataTest.php index bba399026..fb5a212fe 100644 --- a/tests/Metadata/StaticPropertyMetadataTest.php +++ b/tests/Metadata/StaticPropertyMetadataTest.php @@ -14,6 +14,6 @@ public function testSerialization() $this->setNonDefaultMetadataValues($meta); $restoredMeta = unserialize(serialize($meta)); - $this->assertEquals($meta, $restoredMeta); + self::assertEquals($meta, $restoredMeta); } } diff --git a/tests/Metadata/VirtualPropertyMetadataTest.php b/tests/Metadata/VirtualPropertyMetadataTest.php index 090b8cb54..3cfeed608 100644 --- a/tests/Metadata/VirtualPropertyMetadataTest.php +++ b/tests/Metadata/VirtualPropertyMetadataTest.php @@ -15,6 +15,6 @@ public function testSerialization() $this->setNonDefaultMetadataValues($meta); $restoredMeta = unserialize(serialize($meta)); - $this->assertEquals($meta, $restoredMeta); + self::assertEquals($meta, $restoredMeta); } } diff --git a/tests/Serializer/BaseSerializationTest.php b/tests/Serializer/BaseSerializationTest.php index 78492b37c..83f74055b 100644 --- a/tests/Serializer/BaseSerializationTest.php +++ b/tests/Serializer/BaseSerializationTest.php @@ -10,6 +10,9 @@ use JMS\Serializer\DeserializationContext; use JMS\Serializer\EventDispatcher\EventDispatcher; use JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber; +use JMS\Serializer\Exception\ExpressionLanguageRequiredException; +use JMS\Serializer\Exception\InvalidMetadataException; +use JMS\Serializer\Exception\NotAcceptableException; use JMS\Serializer\Exclusion\DepthExclusionStrategy; use JMS\Serializer\Exclusion\GroupsExclusionStrategy; use JMS\Serializer\Expression\ExpressionEvaluator; @@ -48,6 +51,8 @@ use JMS\Serializer\Tests\Fixtures\Discriminator\ImagePost; use JMS\Serializer\Tests\Fixtures\Discriminator\Moped; use JMS\Serializer\Tests\Fixtures\Discriminator\Post; +use JMS\Serializer\Tests\Fixtures\Discriminator\Serialization\ExtendedUser; +use JMS\Serializer\Tests\Fixtures\Discriminator\Serialization\User; use JMS\Serializer\Tests\Fixtures\DiscriminatorGroup\Car as DiscriminatorGroupCar; use JMS\Serializer\Tests\Fixtures\ExclusionStrategy\AlwaysExcludeExclusionStrategy; use JMS\Serializer\Tests\Fixtures\FirstClassListCollection; @@ -88,6 +93,9 @@ use JMS\Serializer\Tests\Fixtures\ParentDoNotSkipWithEmptyChild; use JMS\Serializer\Tests\Fixtures\ParentNoMetadataChildObject; use JMS\Serializer\Tests\Fixtures\ParentSkipWithEmptyChild; +use JMS\Serializer\Tests\Fixtures\PersonAccount; +use JMS\Serializer\Tests\Fixtures\PersonAccountOnParent; +use JMS\Serializer\Tests\Fixtures\PersonAccountWithParent; use JMS\Serializer\Tests\Fixtures\PersonSecret; use JMS\Serializer\Tests\Fixtures\PersonSecretMore; use JMS\Serializer\Tests\Fixtures\PersonSecretMoreVirtual; @@ -101,6 +109,7 @@ use JMS\Serializer\Tests\Fixtures\Tag; use JMS\Serializer\Tests\Fixtures\Timestamp; use JMS\Serializer\Tests\Fixtures\Tree; +use JMS\Serializer\Tests\Fixtures\TypedProperties; use JMS\Serializer\Tests\Fixtures\VehicleInterfaceGarage; use JMS\Serializer\Visitor\DeserializationVisitorInterface; use JMS\Serializer\Visitor\SerializationVisitorInterface; @@ -114,10 +123,11 @@ use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormFactoryBuilder; use Symfony\Component\Translation\IdentityTranslator; -use Symfony\Component\Translation\MessageSelector; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; +use function assert; + abstract class BaseSerializationTest extends TestCase { protected $factory; @@ -149,10 +159,21 @@ public function testSerializeNullArray() ); } + public function testSerializeNullRoot() + { + $context = SerializationContext::create() + ->setAttribute('allows_root_null', true); + + self::assertEquals( + $this->getContent('nullable_root'), + $this->serializer->serialize(null, $this->getFormat(), $context) + ); + } + public function testNoMetadataNeededWhenDeSerializingNotUsedProperty() { - /** @var ParentNoMetadataChildObject $dObj */ $object = $this->deserialize($this->getContent('ParentNoMetadataChildObject'), ParentNoMetadataChildObject::class); + assert($object instanceof ParentNoMetadataChildObject); self::assertSame('John', $object->bar); self::assertNull($object->foo); @@ -160,12 +181,12 @@ public function testNoMetadataNeededWhenDeSerializingNotUsedProperty() public function testDeserializeObjectWithMissingTypedArrayProp() { - /** @var ObjectWithTypedArraySetter $dObj */ $dObj = $this->serializer->deserialize( $this->getContent('empty_object'), ObjectWithTypedArraySetter::class, $this->getFormat() ); + assert($dObj instanceof ObjectWithTypedArraySetter); self::assertInstanceOf(ObjectWithTypedArraySetter::class, $dObj); @@ -211,19 +232,18 @@ public function testDeserializeNullObject() $obj = new ObjectWithNullProperty('foo', 'bar'); - /** @var ObjectWithNullProperty $dObj */ $dObj = $this->serializer->deserialize( $this->getContent('simple_object_nullable'), ObjectWithNullProperty::class, $this->getFormat() ); + assert($dObj instanceof ObjectWithNullProperty); self::assertEquals($obj, $dObj); self::assertNull($dObj->getNullProperty()); } /** - * @expectedException \JMS\Serializer\Exception\NotAcceptableException * @dataProvider getTypes */ public function testNull($type) @@ -234,6 +254,9 @@ public function testNull($type) // this is the default, but we want to be explicit here $context = SerializationContext::create()->setSerializeNull(false); + + $this->expectException(NotAcceptableException::class); + $this->serialize(null, $context); } @@ -271,15 +294,90 @@ public function testString() } } - /** - * @expectedException \JMS\Serializer\Exception\ExpressionLanguageRequiredException - * @expectedExceptionMessage To use conditional exclude/expose in JMS\Serializer\Tests\Fixtures\PersonSecret you must configure the expression language. - */ + public function testExcludeIfOnClass() + { + $accountNotExpired = new PersonAccount(); + $accountNotExpired->name = 'Not expired account'; + $accountNotExpired->expired = false; + + $accountExpired = new PersonAccount(); + $accountExpired->name = 'Expired account'; + $accountExpired->expired = true; + + $accounts = [$accountExpired, $accountNotExpired]; + + $language = new ExpressionLanguage(); + + $builder = SerializerBuilder::create(); + $builder->setExpressionEvaluator(new ExpressionEvaluator($language)); + $serializer = $builder->build(); + + $serialized = $serializer->serialize($accounts, $this->getFormat(), null, sprintf('array<%s>', PersonAccountWithParent::class)); + $deserialized = $serializer->deserialize($serialized, sprintf('array<%s>', PersonAccountWithParent::class), $this->getFormat()); + + $this->assertEquals(1, count($deserialized)); + $this->assertEquals($accountNotExpired->name, $deserialized[0]->name); + } + + public function testExcludeIfOnClassWithParent() + { + $accountNotExpired = new PersonAccountWithParent(); + $accountNotExpired->name = 'Not expired account'; + $accountNotExpired->expired = false; + + $accountExpired = new PersonAccountWithParent(); + $accountExpired->name = 'Expired account'; + $accountExpired->expired = true; + + $accounts = [$accountNotExpired, $accountExpired]; + + $language = new ExpressionLanguage(); + + $builder = SerializerBuilder::create(); + $builder->setExpressionEvaluator(new ExpressionEvaluator($language)); + $serializer = $builder->build(); + + $serialized = $serializer->serialize($accounts, $this->getFormat(), null, sprintf('array<%s>', PersonAccountWithParent::class)); + $deserialized = $serializer->deserialize($serialized, sprintf('array<%s>', PersonAccountWithParent::class), $this->getFormat()); + + $this->assertEquals(1, count($deserialized)); + $this->assertEquals($accountNotExpired->name, $deserialized[0]->name); + } + + public function testExcludeIfOnParentClass() + { + $accountNotExpired = new PersonAccountOnParent(); + $accountNotExpired->name = 'Not expired account'; + $accountNotExpired->expired = false; + + $accountExpired = new PersonAccountOnParent(); + $accountExpired->name = 'Expired account'; + $accountExpired->expired = true; + + $accounts = [$accountNotExpired, $accountExpired]; + + $language = new ExpressionLanguage(); + + $builder = SerializerBuilder::create(); + $builder->setExpressionEvaluator(new ExpressionEvaluator($language)); + $serializer = $builder->build(); + + $serialized = $serializer->serialize($accounts, $this->getFormat(), null, sprintf('array<%s>', PersonAccountOnParent::class)); + $deserialized = $serializer->deserialize($serialized, sprintf('array<%s>', PersonAccountOnParent::class), $this->getFormat()); + + $this->assertEquals(1, count($deserialized)); + $this->assertEquals($accountNotExpired->name, $deserialized[0]->name); + } + public function testExpressionExclusionNotConfigured() { $person = new PersonSecret(); $person->gender = 'f'; $person->name = 'mike'; + + $this->expectException(ExpressionLanguageRequiredException::class); + $this->expectExceptionMessage('To use conditional exclude/expose in JMS\Serializer\Tests\Fixtures\PersonSecret you must configure the expression language.'); + $this->serialize($person); } @@ -448,10 +546,10 @@ public function testSimpleInternalObject() $obj = new SimpleInternalObject('foo', 'bar'); - $this->assertEquals($this->getContent('simple_object'), $this->serialize($obj)); + self::assertEquals($this->getContent('simple_object'), $this->serialize($obj)); if ($this->hasDeserializer()) { - $this->assertEquals($obj, $this->deserialize($this->getContent('simple_object'), get_class($obj))); + self::assertEquals($obj, $this->deserialize($this->getContent('simple_object'), get_class($obj))); } } @@ -466,10 +564,10 @@ public function testSimpleObject() public function testSimpleObjectStaticProp() { - $this->assertEquals($this->getContent('simple_object'), $this->serialize($obj = new SimpleObjectWithStaticProp('foo', 'bar'))); + self::assertEquals($this->getContent('simple_object'), $this->serialize($obj = new SimpleObjectWithStaticProp('foo', 'bar'))); if ($this->hasDeserializer()) { - $this->assertEquals($obj, $this->deserialize($this->getContent('simple_object'), get_class($obj))); + self::assertEquals($obj, $this->deserialize($this->getContent('simple_object'), get_class($obj))); } } @@ -558,8 +656,8 @@ public function testDateTimeArrays() self::assertEquals($this->getContent('array_datetimes_object'), $serializedObject); if ($this->hasDeserializer()) { - /** @var DateTimeArraysObject $deserializedObject */ $deserializedObject = $this->deserialize($this->getContent('array_datetimes_object'), 'Jms\Serializer\Tests\Fixtures\DateTimeArraysObject'); + assert($deserializedObject instanceof DateTimeArraysObject); /** deserialized object has a default timezone set depending on user's timezone settings. That's why we manually set the UTC timezone on the DateTime objects. */ foreach ($deserializedObject->getArrayWithDefaultDateTime() as $dateTime) { @@ -592,8 +690,8 @@ public function testNamedDateTimeArrays() return; } - /** @var NamedDateTimeArraysObject $deserializedObject */ - $deserializedObject = $this->deserialize($this->getContent('array_named_datetimes_object'), 'Jms\Serializer\Tests\Fixtures\NamedDateTimeArraysObject'); + $deserializedObject = $this->deserialize($this->getContent('array_named_datetimes_object'), NamedDateTimeArraysObject::class); + assert($deserializedObject instanceof NamedDateTimeArraysObject); /** deserialized object has a default timezone set depending on user's timezone settings. That's why we manually set the UTC timezone on the DateTime objects. */ foreach ($deserializedObject->getNamedArrayWithFormattedDate() as $dateTime) { @@ -623,8 +721,9 @@ public function testNamedDateTimeImmutableArrays() if ('xml' === $this->getFormat()) { $this->markTestSkipped('XML deserialization does not support key-val pairs mode'); } - /** @var NamedDateTimeArraysObject $deserializedObject */ - $deserializedObject = $this->deserialize($this->getContent('array_named_datetimeimmutables_object'), 'Jms\Serializer\Tests\Fixtures\NamedDateTimeImmutableArraysObject'); + + $deserializedObject = $this->deserialize($this->getContent('array_named_datetimeimmutables_object'), NamedDateTimeImmutableArraysObject::class); + assert($deserializedObject instanceof NamedDateTimeImmutableArraysObject); /** deserialized object has a default timezone set depending on user's timezone settings. That's why we manually set the UTC timezone on the DateTime objects. */ foreach ($deserializedObject->getNamedArrayWithFormattedDate() as $dateTime) { @@ -651,7 +750,7 @@ public function testDateTime($key, $value, $type) if ($this->hasDeserializer()) { $deserialized = $this->deserialize($this->getContent($key), $type); - self::assertInternalType('object', $deserialized); + self::assertIsObject($deserialized); self::assertInstanceOf(get_class($value), $deserialized); self::assertEquals($value->getTimestamp(), $deserialized->getTimestamp()); } @@ -661,6 +760,7 @@ public function getDateTime() { return [ ['date_time', new \DateTime('2011-08-30 00:00', new \DateTimeZone('UTC')), 'DateTime'], + ['date_time_multi_format', new \DateTime('2011-08-30 00:00', new \DateTimeZone('UTC')), "DateTime<'Y-m-d', '', ['Y-m-d','Y-m-d\TH:i:sP']>"], ]; } @@ -675,7 +775,7 @@ public function testDateTimeImmutable($key, $value, $type) if ($this->hasDeserializer()) { $deserialized = $this->deserialize($this->getContent($key), $type); - self::assertInternalType('object', $deserialized); + self::assertIsObject($deserialized); self::assertInstanceOf(get_class($value), $deserialized); self::assertEquals($value->getTimestamp(), $deserialized->getTimestamp()); } @@ -730,14 +830,14 @@ public function testBlogPost() if ($this->hasDeserializer()) { $deserialized = $this->deserialize($this->getContent('blog_post'), get_class($post)); self::assertEquals('2011-07-30T00:00:00+00:00', $this->getField($deserialized, 'createdAt')->format(\DateTime::ATOM)); - self::assertAttributeEquals('This is a nice title.', 'title', $deserialized); - self::assertAttributeSame(false, 'published', $deserialized); - self::assertAttributeSame(false, 'reviewed', $deserialized); - self::assertAttributeSame('e86ce85cdb1253e4fc6352f5cf297248bceec62b', 'etag', $deserialized); - self::assertAttributeEquals(new ArrayCollection([$comment]), 'comments', $deserialized); - self::assertAttributeEquals([$comment], 'comments2', $deserialized); - self::assertAttributeEquals($author, 'author', $deserialized); - self::assertAttributeEquals([$tag1, $tag2], 'tag', $deserialized); + self::assertSame('This is a nice title.', $this->getField($deserialized, 'title')); + self::assertFalse($this->getField($deserialized, 'published')); + self::assertFalse($this->getField($deserialized, 'reviewed')); + self::assertSame('e86ce85cdb1253e4fc6352f5cf297248bceec62b', $this->getField($deserialized, 'etag')); + self::assertEquals(new ArrayCollection([$comment]), $this->getField($deserialized, 'comments')); + self::assertEquals([$comment], $this->getField($deserialized, 'comments2')); + self::assertEquals($author, $this->getField($deserialized, 'author')); + self::assertEquals([$tag1, $tag2], $this->getField($deserialized, 'tag')); } } @@ -760,10 +860,10 @@ public function testDeserializingNull() $deserialized = $this->deserialize($this->getContent('blog_post_unauthored'), get_class($post), DeserializationContext::create()); self::assertEquals('2011-07-30T00:00:00+00:00', $this->getField($deserialized, 'createdAt')->format(\DateTime::ATOM)); - self::assertAttributeEquals('This is a nice title.', 'title', $deserialized); - self::assertAttributeSame(false, 'published', $deserialized); - self::assertAttributeSame(false, 'reviewed', $deserialized); - self::assertAttributeEquals(new ArrayCollection(), 'comments', $deserialized); + self::assertSame('This is a nice title.', $this->getField($deserialized, 'title')); + self::assertFalse($this->getField($deserialized, 'published')); + self::assertFalse($this->getField($deserialized, 'reviewed')); + self::assertEquals(new ArrayCollection(), $this->getField($deserialized, 'comments')); self::assertEquals(null, $this->getField($deserialized, 'author')); } } @@ -792,15 +892,14 @@ public function testExpressionAuthorWithContextVars() self::assertEquals($this->getContent('author_expression_context'), $serializer->serialize($author, $this->getFormat())); } - - /** - * @expectedException \JMS\Serializer\Exception\ExpressionLanguageRequiredException - * @expectedExceptionMessage The property firstName on JMS\Serializer\Tests\Fixtures\AuthorExpressionAccess requires the expression accessor strategy to be enabled. - */ public function testExpressionAccessorStrategNotEnabled() { $author = new AuthorExpressionAccess(123, 'Ruud', 'Kamphuis'); - self::assertEquals($this->getContent('author_expression'), $this->serialize($author)); + + $this->expectException(ExpressionLanguageRequiredException::class); + $this->expectExceptionMessage('The property firstName on JMS\Serializer\Tests\Fixtures\AuthorExpressionAccess requires the expression accessor strategy to be enabled.'); + + $this->serialize($author); } public function testReadOnly() @@ -973,7 +1072,7 @@ public function testLifecycleCallbacks() { $object = new ObjectWithLifecycleCallbacks(); self::assertEquals($this->getContent('lifecycle_callbacks'), $this->serialize($object)); - self::assertAttributeSame(null, 'name', $object); + self::assertNull($this->getField($object, 'name')); if ($this->hasDeserializer()) { $deserialized = $this->deserialize($this->getContent('lifecycle_callbacks'), get_class($object)); @@ -1098,9 +1197,9 @@ public function testMixedAccessTypes() if ($this->hasDeserializer()) { $object = $this->deserialize($this->getContent('mixed_access_types'), 'JMS\Serializer\Tests\Fixtures\GetSetObject'); - self::assertAttributeEquals(1, 'id', $object); - self::assertAttributeEquals('Johannes', 'name', $object); - self::assertAttributeEquals(42, 'readOnlyProperty', $object); + self::assertSame(1, $this->getField($object, 'id')); + self::assertSame('Johannes', $this->getField($object, 'name')); + self::assertSame(42, $this->getField($object, 'readOnlyProperty')); } } @@ -1195,14 +1294,13 @@ public function testAdvancedGroups() ); } - /** - * @expectedException \JMS\Serializer\Exception\InvalidMetadataException - * @expectedExceptionMessage Invalid group name "foo, bar" on "JMS\Serializer\Tests\Fixtures\InvalidGroupsObject->foo", did you mean to create multiple groups? - */ public function testInvalidGroupName() { $groupsObject = new InvalidGroupsObject(); + $this->expectException(InvalidMetadataException::class); + $this->expectExceptionMessage('Invalid group name "foo, bar" on "JMS\Serializer\Tests\Fixtures\InvalidGroupsObject->foo", did you mean to create multiple groups?'); + $this->serializer->serialize($groupsObject, $this->getFormat()); } @@ -1252,6 +1350,39 @@ public function testCustomHandler() self::assertEquals('customly_unserialized_value', $object->someProperty); } + public function testTypedProperties() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped(sprintf('%s requires PHP 7.4', __METHOD__)); + } + + $builder = SerializerBuilder::create($this->handlerRegistry, $this->dispatcher); + $builder->includeInterfaceMetadata(true); + $this->serializer = $builder->build(); + + $user = new TypedProperties\User(); + $user->id = 1; + $user->created = new \DateTime('2010-10-01 00:00:00'); + $user->updated = new \DateTime('2011-10-01 00:00:00'); + $user->tags = ['a', 'b']; + $role = new TypedProperties\Role(); + $role->id = 5; + $user->role = $role; + $user->vehicle = new TypedProperties\Car(); + + $result = $this->serialize($user); + + self::assertEquals($this->getContent('typed_props'), $result); + + if ($this->hasDeserializer()) { + // updated is read only + $user->updated = null; + $user->tags = []; + + self::assertEquals($user, $this->deserialize($this->getContent('typed_props'), get_class($user))); + } + } + /** * @doesNotPerformAssertions */ @@ -1308,6 +1439,33 @@ public function testPolymorphicObjectsWithGroup() ); } + public function getDiscrimatorObjectsSamples(): array + { + $u1 = new User(5, 'userName', 'userDesc'); + $u2 = new ExtendedUser(5, 'userName', 'userDesc', 'extednedContent'); + $arr = new ArrayCollection([$u1, $u2]); + + return [ + [$u1, 'user_discriminator'], + [$u2, 'user_discriminator_extended'], + [$arr, 'user_discriminator_array'], + ]; + } + + /** + * Test serializing entity that uses Discriminator and extends some base model class + * + * @dataProvider getDiscrimatorObjectsSamples + */ + public function testDiscrimatorObjects($data, $contentId) + { + $context = SerializationContext::create()->setGroups(['entity.identification']); + self::assertEquals( + $this->getContent($contentId), + $this->serialize($data, $context) + ); + } + /** * @group polymorphic */ @@ -1429,10 +1587,11 @@ public function testNestedPolymorphicInterfaces() /** * @group polymorphic - * @expectedException LogicException */ public function testPolymorphicObjectsInvalidDeserialization() { + $this->expectException(\LogicException::class); + if (!$this->hasDeserializer()) { throw new \LogicException('No deserializer'); } @@ -1478,6 +1637,7 @@ public function testDeserializingIntoExistingObject() if (!$this->hasDeserializer()) { return; } + $objectConstructor = new InitializedObjectConstructor(new UnserializeObjectConstructor()); $builder = SerializerBuilder::create(); @@ -1498,7 +1658,7 @@ public function testDeserializingIntoExistingObject() self::assertSame($order, $deseralizedOrder); self::assertEquals(new Order(new Price(12.34)), $deseralizedOrder); - self::assertAttributeInstanceOf('JMS\Serializer\Tests\Fixtures\Price', 'cost', $deseralizedOrder); + self::assertInstanceOf(Price::class, $this->getField($deseralizedOrder, 'cost')); } public function testObjectWithNullableArrays() @@ -1533,15 +1693,16 @@ public function testHandlerInvokedOnPrimitives() GraphNavigatorInterface::DIRECTION_SERIALIZATION, 'Virtual', $this->getFormat(), - function ($visitor, $data) use (&$invoked) { + static function ($visitor, $data) use (&$invoked) { $invoked = true; - $this->assertEquals('foo', $data); + self::assertEquals('foo', $data); + return null; } ); $this->serializer->serialize('foo', $this->getFormat(), null, 'Virtual'); - $this->assertTrue($invoked); + self::assertTrue($invoked); } public function getFirstClassListCollectionsValues() @@ -1591,6 +1752,7 @@ public function testIterable(): void if (!$this->hasDeserializer()) { return; } + self::assertEquals( new ObjectWithIterable(Functions::iterableToArray($generator())), $this->deserialize($this->getContent('iterable'), get_class($withIterable)) @@ -1609,6 +1771,7 @@ public function testGenerator(): void if (!$this->hasDeserializer()) { return; } + self::assertEquals( $withGenerator, $this->deserialize($this->getContent('generator'), get_class($withGenerator)) @@ -1627,6 +1790,7 @@ public function testIterator(): void if (!$this->hasDeserializer()) { return; } + self::assertEquals( $withIterator, $this->deserialize($this->getContent('iterator'), get_class($withIterator)) @@ -1645,6 +1809,7 @@ public function testArrayIterator(): void if (!$this->hasDeserializer()) { return; } + self::assertEquals( $withArrayIterator, $this->deserialize($this->getContent('iterator'), get_class($withArrayIterator)) @@ -1684,7 +1849,7 @@ protected function setUp(): void $this->handlerRegistry->registerSubscribingHandler(new ConstraintViolationHandler()); $this->handlerRegistry->registerSubscribingHandler(new StdClassHandler()); $this->handlerRegistry->registerSubscribingHandler(new DateHandler()); - $this->handlerRegistry->registerSubscribingHandler(new FormErrorHandler(new IdentityTranslator(new MessageSelector()))); + $this->handlerRegistry->registerSubscribingHandler(new FormErrorHandler(new IdentityTranslator())); $this->handlerRegistry->registerSubscribingHandler(new ArrayCollectionHandler()); $this->handlerRegistry->registerSubscribingHandler(new IteratorHandler()); $this->handlerRegistry->registerHandler( diff --git a/tests/Serializer/ContextTest.php b/tests/Serializer/ContextTest.php index 1a5550441..ef2f7eb14 100644 --- a/tests/Serializer/ContextTest.php +++ b/tests/Serializer/ContextTest.php @@ -4,6 +4,8 @@ namespace JMS\Serializer\Tests\Serializer; +use JMS\Serializer\Exception\LogicException; +use JMS\Serializer\GraphNavigator; use JMS\Serializer\Metadata\ClassMetadata; use JMS\Serializer\Metadata\PropertyMetadata; use JMS\Serializer\SerializationContext; @@ -14,6 +16,8 @@ use JMS\Serializer\Tests\Fixtures\Node; use JMS\Serializer\Tests\Fixtures\Publisher; use JMS\Serializer\Tests\Fixtures\VersionedObject; +use JMS\Serializer\VisitorInterface; +use Metadata\MetadataFactoryInterface; use PHPUnit\Framework\TestCase; class ContextTest extends TestCase @@ -214,4 +218,20 @@ public function testSerializeNullOption() $context->setSerializeNull(true); self::assertTrue($context->shouldSerializeNull()); } + + public function testContextBecomesImmutableWhenStartingProcess() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('This context was already initialized and is immutable; you cannot modify it anymore.'); + + $visitor = $this->createMock(VisitorInterface::class); + $navigator = $this->createMock(GraphNavigator::class); + $metadataFactory = $this->createMock(MetadataFactoryInterface::class); + + $context = SerializationContext::create(); + + $context->initialize('json', $visitor, $navigator, $metadataFactory); + + $context->setSerializeNull(false); + } } diff --git a/tests/Serializer/Doctrine/IntegrationTest.php b/tests/Serializer/Doctrine/IntegrationTest.php index 1288b14bd..579e52b3b 100644 --- a/tests/Serializer/Doctrine/IntegrationTest.php +++ b/tests/Serializer/Doctrine/IntegrationTest.php @@ -6,8 +6,6 @@ use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\Reader; -use Doctrine\Common\Persistence\AbstractManagerRegistry; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\DBAL\Connection; use Doctrine\DBAL\DriverManager; use Doctrine\ORM\Configuration; @@ -15,6 +13,8 @@ use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\ORMException; use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\Persistence\AbstractManagerRegistry; +use Doctrine\Persistence\Proxy; use JMS\Serializer\Builder\CallbackDriverFactory; use JMS\Serializer\Builder\DefaultDriverFactory; use JMS\Serializer\Metadata\Driver\DoctrineTypeDriver; @@ -31,7 +31,7 @@ class IntegrationTest extends TestCase { - /** @var ManagerRegistry */ + /** @var AbstractManagerRegistry */ private $registry; /** @var Serializer */ @@ -59,8 +59,8 @@ public function testDiscriminatorIsInferredForGenericBaseClass() public function testDiscriminatorIsInferredFromDoctrine() { - /** @var EntityManager $em */ $em = $this->registry->getManager(); + \assert($em instanceof EntityManager); $student1 = new Student(); $student2 = new Student(); @@ -116,8 +116,8 @@ static function (array $metadataDirs, Reader $annotationReader) use ($registry) private function prepareDatabase() { - /** @var EntityManager $em */ $em = $this->registry->getManager(); + \assert($em instanceof EntityManager); $tool = new SchemaTool($em); $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); @@ -150,11 +150,12 @@ class SimpleManagerRegistry extends AbstractManagerRegistry private $services = []; private $serviceCreator; - public function __construct($serviceCreator, $name = 'anonymous', array $connections = ['default' => 'default_connection'], array $managers = ['default' => 'default_manager'], $defaultConnection = null, $defaultManager = null, $proxyInterface = 'Doctrine\Common\Persistence\Proxy') + public function __construct($serviceCreator, $name = 'anonymous', array $connections = ['default' => 'default_connection'], array $managers = ['default' => 'default_manager'], $defaultConnection = null, $defaultManager = null, $proxyInterface = Proxy::class) { if (null === $defaultConnection) { $defaultConnection = key($connections); } + if (null === $defaultManager) { $defaultManager = key($managers); } @@ -164,6 +165,7 @@ public function __construct($serviceCreator, $name = 'anonymous', array $connect if (!is_callable($serviceCreator)) { throw new \InvalidArgumentException('$serviceCreator must be a valid callable.'); } + $this->serviceCreator = $serviceCreator; } diff --git a/tests/Serializer/Doctrine/ObjectConstructorTest.php b/tests/Serializer/Doctrine/ObjectConstructorTest.php index 1c48e68dd..1faeece44 100644 --- a/tests/Serializer/Doctrine/ObjectConstructorTest.php +++ b/tests/Serializer/Doctrine/ObjectConstructorTest.php @@ -6,8 +6,6 @@ use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\Reader; -use Doctrine\Common\Persistence\AbstractManagerRegistry; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\DBAL\Connection; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Types\Type; @@ -19,12 +17,17 @@ use Doctrine\ORM\Tools\SchemaTool; use Doctrine\ORM\UnitOfWork; use Doctrine\ORM\Version as ORMVersion; +use Doctrine\Persistence\AbstractManagerRegistry; +use Doctrine\Persistence\Proxy; use JMS\Serializer\Builder\CallbackDriverFactory; use JMS\Serializer\Builder\DefaultDriverFactory; use JMS\Serializer\Construction\DoctrineObjectConstructor; use JMS\Serializer\Construction\ObjectConstructorInterface; use JMS\Serializer\Construction\UnserializeObjectConstructor; use JMS\Serializer\DeserializationContext; +use JMS\Serializer\Exception\InvalidArgumentException; +use JMS\Serializer\Exception\ObjectConstructionException; +use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\Metadata\ClassMetadata; use JMS\Serializer\Metadata\Driver\DoctrineTypeDriver; use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy; @@ -33,14 +36,17 @@ use JMS\Serializer\SerializerInterface; use JMS\Serializer\Tests\Fixtures\Doctrine\Embeddable\BlogPostSeo; use JMS\Serializer\Tests\Fixtures\Doctrine\Entity\Author; +use JMS\Serializer\Tests\Fixtures\Doctrine\Entity\AuthorExcludedId; use JMS\Serializer\Tests\Fixtures\Doctrine\IdentityFields\Server; use JMS\Serializer\Tests\Fixtures\DoctrinePHPCR\Author as DoctrinePHPCRAuthor; use JMS\Serializer\Visitor\DeserializationVisitorInterface; +use Metadata\Driver\AdvancedDriverInterface; +use Metadata\MetadataFactoryInterface; use PHPUnit\Framework\TestCase; class ObjectConstructorTest extends TestCase { - /** @var ManagerRegistry */ + /** @var AbstractManagerRegistry */ private $registry; /** @var Serializer */ @@ -52,6 +58,9 @@ class ObjectConstructorTest extends TestCase /** @var DeserializationContext */ private $context; + /** @var AdvancedDriverInterface */ + private $driver; + public function testFindEntity() { $em = $this->registry->getManager(); @@ -64,7 +73,7 @@ public function testFindEntity() $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock(); $type = ['name' => Author::class, 'params' => []]; - $class = new ClassMetadata(Author::class); + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(Author::class)); $constructor = new DoctrineObjectConstructor($this->registry, $fallback); $authorFetched = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context); @@ -72,6 +81,41 @@ public function testFindEntity() self::assertEquals($author, $authorFetched); } + public function testFindEntityExcludedByGroupsUsesFallback() + { + $graph = $this->createMock(GraphNavigatorInterface::class); + $metadata = $this->createMock(MetadataFactoryInterface::class); + + $author = new Author('John'); + $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock(); + $fallback->expects($this->once())->method('construct')->willReturn($author); + + $type = ['name' => Author::class, 'params' => []]; + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(Author::class)); + + $context = DeserializationContext::create()->setGroups('foo'); + $context->initialize('json', $this->visitor, $graph, $metadata); + $constructor = new DoctrineObjectConstructor($this->registry, $fallback); + $authorFetched = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $context); + + self::assertSame($author, $authorFetched); + } + + public function testFindEntityExcludedUsesFallback() + { + $author = new Author('John'); + $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock(); + $fallback->expects($this->once())->method('construct')->willReturn($author); + + $type = ['name' => AuthorExcludedId::class, 'params' => []]; + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(AuthorExcludedId::class)); + + $constructor = new DoctrineObjectConstructor($this->registry, $fallback); + $authorFetched = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context); + + self::assertSame($author, $authorFetched); + } + public function testFindManagedEntity() { $em = $this->registry->getManager(); @@ -83,7 +127,7 @@ public function testFindManagedEntity() $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock(); $type = ['name' => Author::class, 'params' => []]; - $class = new ClassMetadata(Author::class); + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(Author::class)); $constructor = new DoctrineObjectConstructor($this->registry, $fallback); $authorFetched = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context); @@ -96,7 +140,7 @@ public function testMissingAuthor() $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock(); $type = ['name' => Author::class, 'params' => []]; - $class = new ClassMetadata(Author::class); + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(Author::class)); $constructor = new DoctrineObjectConstructor($this->registry, $fallback); $author = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context); @@ -111,7 +155,7 @@ public function testMissingAuthorFallback() $fallback->expects($this->once())->method('construct')->willReturn($author); $type = ['name' => Author::class, 'params' => []]; - $class = new ClassMetadata(Author::class); + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(Author::class)); $constructor = new DoctrineObjectConstructor($this->registry, $fallback, DoctrineObjectConstructor::ON_MISSING_FALLBACK); $authorFetched = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context); @@ -126,7 +170,7 @@ public function testMissingNotManaged() $fallback->expects($this->once())->method('construct')->willReturn($author); $type = ['name' => Author::class, 'params' => []]; - $class = new ClassMetadata(Author::class); + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(Author::class)); $constructor = new DoctrineObjectConstructor($this->registry, $fallback, DoctrineObjectConstructor::ON_MISSING_FALLBACK); $authorFetched = $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context); @@ -144,38 +188,38 @@ public function testReference() $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock(); $type = ['name' => Author::class, 'params' => []]; - $class = new ClassMetadata(Author::class); + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(Author::class)); $constructor = new DoctrineObjectConstructor($this->registry, $fallback, DoctrineObjectConstructor::ON_MISSING_FALLBACK); $authorFetched = $constructor->construct($this->visitor, $class, 5, $type, $this->context); self::assertSame($author, $authorFetched); } - /** - * @expectedException \JMS\Serializer\Exception\ObjectConstructionException - */ public function testMissingAuthorException() { $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock(); $type = ['name' => Author::class, 'params' => []]; - $class = new ClassMetadata(Author::class); + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(Author::class)); $constructor = new DoctrineObjectConstructor($this->registry, $fallback, DoctrineObjectConstructor::ON_MISSING_EXCEPTION); + + $this->expectException(ObjectConstructionException::class); + $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context); } - /** - * @expectedException \JMS\Serializer\Exception\InvalidArgumentException - */ public function testInvalidArg() { $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock(); $type = ['name' => Author::class, 'params' => []]; - $class = new ClassMetadata(Author::class); + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(Author::class)); $constructor = new DoctrineObjectConstructor($this->registry, $fallback, 'foo'); + + $this->expectException(InvalidArgumentException::class); + $constructor->construct($this->visitor, $class, ['id' => 5], $type, $this->context); } @@ -187,7 +231,7 @@ public function testMissingData() $fallback->expects($this->once())->method('construct')->willReturn($author); $type = ['name' => Author::class, 'params' => []]; - $class = new ClassMetadata(Author::class); + $class = $this->driver->loadMetadataForClass(new \ReflectionClass(Author::class)); $constructor = new DoctrineObjectConstructor($this->registry, $fallback, 'foo'); $authorFetched = $constructor->construct($this->visitor, $class, ['foo' => 5], $type, $this->context); @@ -198,18 +242,18 @@ public function testNamingForIdentifierColumnIsConsidered() { $serializer = $this->createSerializerWithDoctrineObjectConstructor(); - /** @var EntityManager $em */ $em = $this->registry->getManager(); + \assert($em instanceof EntityManager); $server = new Server('Linux', '127.0.0.1', 'home'); $em->persist($server); $em->flush(); $em->clear(); $jsonData = '{"ip_address":"127.0.0.1", "server_id_extracted":"home", "name":"Windows"}'; - /** @var Server $serverDeserialized */ $serverDeserialized = $serializer->deserialize($jsonData, Server::class, 'json'); + \assert($serverDeserialized instanceof Server); - static::assertSame( + self::assertSame( $em->getUnitOfWork()->getEntityState($serverDeserialized), UnitOfWork::STATE_MANAGED ); @@ -249,6 +293,41 @@ static function ($id) use ($connection, $entityManager) { $constructor->construct($this->visitor, $class, ['metaTitle' => 'test'], $type, $this->context); } + public function testFallbackOnEmbeddableClassWithXmlDriverAndXmlData() + { + if (ORMVersion::compare('2.5') >= 0) { + $this->markTestSkipped('Not using Doctrine ORM >= 2.5 with Embedded entities'); + } + + $fallback = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock(); + $fallback->expects($this->once())->method('construct'); + + $connection = $this->createConnection(); + $entityManager = $this->createXmlEntityManager($connection); + + $this->registry = $registry = new SimpleBaseManagerRegistry( + static function ($id) use ($connection, $entityManager) { + switch ($id) { + case 'default_connection': + return $connection; + + case 'default_manager': + return $entityManager; + + default: + throw new \RuntimeException(sprintf('Unknown service id "%s".', $id)); + } + } + ); + + $type = ['name' => BlogPostSeo::class, 'params' => []]; + $class = new ClassMetadata(BlogPostSeo::class); + + $data = new \SimpleXMLElement('test'); + $constructor = new DoctrineObjectConstructor($this->registry, $fallback, DoctrineObjectConstructor::ON_MISSING_FALLBACK); + $constructor->construct($this->visitor, $class, $data, $type, $this->context); + } + protected function setUp(): void { $this->visitor = $this->getMockBuilder(DeserializationVisitorInterface::class)->getMock(); @@ -271,13 +350,14 @@ static function ($id) use ($connection, $entityManager) { } } ); - + $driver = null; + $this->driver = &$driver; $this->serializer = SerializerBuilder::create() ->setMetadataDriverFactory(new CallbackDriverFactory( - static function (array $metadataDirs, Reader $annotationReader) use ($registry) { + static function (array $metadataDirs, Reader $annotationReader) use ($registry, &$driver) { $defaultFactory = new DefaultDriverFactory(new IdenticalPropertyNamingStrategy()); - return new DoctrineTypeDriver($defaultFactory->createDriver($metadataDirs, $annotationReader), $registry); + return $driver = new DoctrineTypeDriver($defaultFactory->createDriver($metadataDirs, $annotationReader), $registry); } )) ->build(); @@ -287,8 +367,8 @@ static function (array $metadataDirs, Reader $annotationReader) use ($registry) private function prepareDatabase() { - /** @var EntityManager $em */ $em = $this->registry->getManager(); + \assert($em instanceof EntityManager); $tool = new SchemaTool($em); $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); @@ -360,11 +440,12 @@ class SimpleBaseManagerRegistry extends AbstractManagerRegistry private $services = []; private $serviceCreator; - public function __construct($serviceCreator, $name = 'anonymous', array $connections = ['default' => 'default_connection'], array $managers = ['default' => 'default_manager'], $defaultConnection = null, $defaultManager = null, $proxyInterface = 'Doctrine\Common\Persistence\Proxy') + public function __construct($serviceCreator, $name = 'anonymous', array $connections = ['default' => 'default_connection'], array $managers = ['default' => 'default_manager'], $defaultConnection = null, $defaultManager = null, $proxyInterface = Proxy::class) { if (null === $defaultConnection) { $defaultConnection = key($connections); } + if (null === $defaultManager) { $defaultManager = key($managers); } @@ -374,6 +455,7 @@ public function __construct($serviceCreator, $name = 'anonymous', array $connect if (!is_callable($serviceCreator)) { throw new \InvalidArgumentException('$serviceCreator must be a valid callable.'); } + $this->serviceCreator = $serviceCreator; } diff --git a/tests/Serializer/EventDispatcher/EventDispatcherTest.php b/tests/Serializer/EventDispatcher/EventDispatcherTest.php index 917ce9a27..7a05ca644 100644 --- a/tests/Serializer/EventDispatcher/EventDispatcherTest.php +++ b/tests/Serializer/EventDispatcher/EventDispatcherTest.php @@ -4,7 +4,7 @@ namespace JMS\Serializer\Tests\Serializer\EventDispatcher; -use Doctrine\Common\Persistence\Proxy; +use Doctrine\Persistence\Proxy; use JMS\Serializer\Context; use JMS\Serializer\EventDispatcher\Event; use JMS\Serializer\EventDispatcher\EventDispatcher; @@ -56,7 +56,7 @@ public function testDispatch() $a = new MockListener(); $this->dispatcher->addListener('foo', [$a, 'Foo']); $this->dispatch('bar'); - $a->_verify('Listener is not called for other event.'); + $a->verify('Listener is not called for other event.'); $b = new MockListener(); $this->dispatcher->addListener('pre', [$b, 'bar'], 'Bar'); @@ -68,10 +68,10 @@ public function testDispatch() $b->foo($this->event, 'pre', 'Foo', 'json', $this->dispatcher); $b->all($this->event, 'pre', 'Foo', 'json', $this->dispatcher); - $b->_replay(); + $b->replay(); $this->dispatch('pre', 'Bar'); $this->dispatch('pre', 'Foo'); - $b->_verify(); + $b->verify(); } public function testDispatchWithInstanceFilteringBothListenersInvoked() @@ -88,9 +88,9 @@ public function testDispatchWithInstanceFilteringBothListenersInvoked() $a->onlyProxy($event, 'pre', 'Bar', 'json', $this->dispatcher); $a->all($event, 'pre', 'Bar', 'json', $this->dispatcher); - $a->_replay(); + $a->replay(); $this->dispatch('pre', 'Bar', 'json', $event); - $a->_verify(); + $a->verify(); } public function testDispatchWithInstanceFilteringOnlyGenericListenerInvoked() @@ -106,9 +106,9 @@ public function testDispatchWithInstanceFilteringOnlyGenericListenerInvoked() // expected $a->all($event, 'pre', 'Bar', 'json', $this->dispatcher); - $a->_replay(); + $a->replay(); $this->dispatch('pre', 'Bar', 'json', $event); - $a->_verify(); + $a->verify(); } public function testListenerCanStopPropagation() @@ -177,14 +177,18 @@ public function testAddSubscriber() ]; $this->dispatcher->addSubscriber($subscriber); - self::assertAttributeEquals([ + + $listenersReflection = new \ReflectionProperty(EventDispatcher::class, 'listeners'); + $listenersReflection->setAccessible(true); + + self::assertSame([ 'foo.bar_baz' => [ [[$subscriber, 'onfoobarbaz'], null, 'foo', null], ], 'bar' => [ [[$subscriber, 'bar'], 'foo', null, null], ], - ], 'listeners', $this->dispatcher); + ], $listenersReflection->getValue($this->dispatcher)); } protected function setUp(): void @@ -233,12 +237,12 @@ public function __call($method, array $args = []) $this->actual[] = [$method, $args]; } - public function _replay() + public function replay() { $this->wasReplayed = true; } - public function _verify($message = '') + public function verify($message = '') { Assert::assertSame($this->expected, $this->actual, $message); } diff --git a/tests/Serializer/EventDispatcher/LazyEventDispatcherTest.php b/tests/Serializer/EventDispatcher/LazyEventDispatcherTest.php index fb3d0321c..ae2602e53 100644 --- a/tests/Serializer/EventDispatcher/LazyEventDispatcherTest.php +++ b/tests/Serializer/EventDispatcher/LazyEventDispatcherTest.php @@ -34,7 +34,7 @@ public function testDispatchWithListenerAsService() $this->dispatcher->addListener('foo', ['a', 'foo']); $this->dispatch('bar'); - $a->_verify('Listener is not called for other event.'); + $a->verify('Listener is not called for other event.'); $b = new MockListener(); $this->registerListenerService('b', $b); @@ -47,10 +47,10 @@ public function testDispatchWithListenerAsService() $b->all($this->event, 'pre', 'Bar', 'json', $this->dispatcher); $b->foo($this->event, 'pre', 'Foo', 'json', $this->dispatcher); $b->all($this->event, 'pre', 'Foo', 'json', $this->dispatcher); - $b->_replay(); + $b->replay(); $this->dispatch('pre', 'Bar'); $this->dispatch('pre', 'Foo'); - $b->_verify(); + $b->verify(); } protected function createEventDispatcher() diff --git a/tests/Serializer/EventDispatcher/Subscriber/DoctrineProxySubscriberTest.php b/tests/Serializer/EventDispatcher/Subscriber/DoctrineProxySubscriberTest.php index 00c9e8b54..040632dd4 100644 --- a/tests/Serializer/EventDispatcher/Subscriber/DoctrineProxySubscriberTest.php +++ b/tests/Serializer/EventDispatcher/Subscriber/DoctrineProxySubscriberTest.php @@ -11,6 +11,7 @@ use JMS\Serializer\Metadata\ClassMetadata; use JMS\Serializer\Tests\Fixtures\ExclusionStrategy\AlwaysExcludeExclusionStrategy; use JMS\Serializer\Tests\Fixtures\SimpleObject; +use JMS\Serializer\Tests\Fixtures\SimpleObjectLazyLoading; use JMS\Serializer\Tests\Fixtures\SimpleObjectProxy; use Metadata\MetadataFactoryInterface; use PHPUnit\Framework\TestCase; @@ -146,6 +147,15 @@ public function testOnPreSerializeMaintainsParams() self::assertSame(['name' => SimpleObject::class, 'params' => ['baz']], $event->getType()); } + public function testRewritesLazyLoadingClassName() + { + $event = $this->createEvent($obj = new SimpleObjectLazyLoading('a', 'b'), ['name' => get_class($obj), 'params' => []]); + $this->subscriber->onPreSerialize($event); + + self::assertEquals(['name' => get_parent_class($obj), 'params' => []], $event->getType()); + self::assertTrue($obj->__isInitialized()); + } + protected function setUp(): void { $this->subscriber = new DoctrineProxySubscriber(); diff --git a/tests/Serializer/EventDispatcher/Subscriber/SymfonyValidatorValidatorSubscriberTest.php b/tests/Serializer/EventDispatcher/Subscriber/SymfonyValidatorValidatorSubscriberTest.php index 831131ace..2e03ebb02 100644 --- a/tests/Serializer/EventDispatcher/Subscriber/SymfonyValidatorValidatorSubscriberTest.php +++ b/tests/Serializer/EventDispatcher/Subscriber/SymfonyValidatorValidatorSubscriberTest.php @@ -9,6 +9,7 @@ use JMS\Serializer\EventDispatcher\ObjectEvent; use JMS\Serializer\EventDispatcher\Subscriber\SymfonyValidatorSubscriber; use JMS\Serializer\EventDispatcher\Subscriber\SymfonyValidatorValidatorSubscriber; +use JMS\Serializer\Exception\ValidationFailedException; use JMS\Serializer\SerializerBuilder; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\ConstraintViolation; @@ -35,10 +36,6 @@ public function testValidate() $this->subscriber->onPostDeserialize(new ObjectEvent($context, $obj, [])); } - /** - * @expectedException \JMS\Serializer\Exception\ValidationFailedException - * @expectedExceptionMessage Validation failed with 1 error(s). - */ public function testValidateThrowsExceptionWhenListIsNotEmpty() { $obj = new \stdClass(); @@ -50,6 +47,9 @@ public function testValidateThrowsExceptionWhenListIsNotEmpty() $context = DeserializationContext::create()->setAttribute('validation_groups', ['foo']); + $this->expectException(ValidationFailedException::class); + $this->expectExceptionMessage('Validation failed with 1 error(s).'); + $this->subscriber->onPostDeserialize(new ObjectEvent($context, $obj, [])); } diff --git a/tests/Serializer/GraphNavigatorTest.php b/tests/Serializer/GraphNavigatorTest.php index ff57b7a35..6d9273357 100644 --- a/tests/Serializer/GraphNavigatorTest.php +++ b/tests/Serializer/GraphNavigatorTest.php @@ -6,9 +6,14 @@ use Doctrine\Common\Annotations\AnnotationReader; use JMS\Serializer\Accessor\DefaultAccessorStrategy; +use JMS\Serializer\Construction\ObjectConstructorInterface; use JMS\Serializer\Construction\UnserializeObjectConstructor; +use JMS\Serializer\Context; use JMS\Serializer\DeserializationContext; use JMS\Serializer\EventDispatcher\EventDispatcher; +use JMS\Serializer\Exception\NotAcceptableException; +use JMS\Serializer\Exception\RuntimeException; +use JMS\Serializer\Exception\SkipHandlerException; use JMS\Serializer\Exclusion\ExclusionStrategyInterface; use JMS\Serializer\GraphNavigator\DeserializationGraphNavigator; use JMS\Serializer\GraphNavigator\SerializationGraphNavigator; @@ -20,6 +25,7 @@ use JMS\Serializer\SerializationContext; use JMS\Serializer\Visitor\DeserializationVisitorInterface; use JMS\Serializer\Visitor\SerializationVisitorInterface; +use JMS\Serializer\VisitorInterface; use Metadata\MetadataFactory; use PHPUnit\Framework\TestCase; @@ -38,12 +44,11 @@ class GraphNavigatorTest extends TestCase private $serializationVisitor; private $deserializationVisitor; - /** - * @expectedException JMS\Serializer\Exception\RuntimeException - * @expectedExceptionMessage Resources are not supported in serialized data. - */ public function testResourceThrowsException() { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Resources are not supported in serialized data.'); + $this->serializationNavigator->accept(STDIN, null); } @@ -61,6 +66,7 @@ public function testNavigatorPassesInstanceOnSerialization() ->will($this->returnCallback(static function ($passedMetadata, $passedContext) use ($metadata, $context, $self) { $self->assertSame($metadata, $passedMetadata); $self->assertSame($context, $passedContext); + return false; })); $exclusionStrategy->expects($this->once()) @@ -68,6 +74,7 @@ public function testNavigatorPassesInstanceOnSerialization() ->will($this->returnCallback(static function ($propertyMetadata, $passedContext) use ($context, $metadata, $self) { $self->assertSame($metadata->propertyMetadata['foo'], $propertyMetadata); $self->assertSame($context, $passedContext); + return false; })); @@ -128,9 +135,96 @@ public function testNavigatorChangeTypeOnSerialization() $navigator = new SerializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $this->accessor, $this->dispatcher); $navigator->initialize($this->serializationVisitor, $this->context); + $this->context->initialize(TestSubscribingHandler::FORMAT, $this->serializationVisitor, $navigator, $this->metadataFactory); + $navigator->accept($object, null); } + public function testExposeAcceptHandlerExceptionOnSerialization() + { + $object = new SerializableClass(); + $typeName = 'JsonSerializable'; + $msg = 'Useful serialization error with relevant context information'; + + $handler = static function ($visitor, $data, array $type, SerializationContext $context) use ($msg) { + $context->startVisiting(new \stdClass()); + + throw new \RuntimeException($msg); + }; + $this->handlerRegistry->registerHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $typeName, TestSubscribingHandler::FORMAT, $handler); + + $this->context->method('getFormat')->willReturn(TestSubscribingHandler::FORMAT); + + $navigator = new SerializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $this->accessor, $this->dispatcher); + $navigator->initialize($this->serializationVisitor, $this->context); + $this->context->initialize(TestSubscribingHandler::FORMAT, $this->serializationVisitor, $navigator, $this->metadataFactory); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage($msg); + $navigator->accept($object, ['name' => $typeName, 'params' => []]); + } + + public function testHandlerIsExecutedOnSerialization() + { + $object = new SerializableClass(); + $this->handlerRegistry->registerSubscribingHandler(new TestSubscribingHandler()); + + $this->context->method('getFormat')->willReturn(TestSubscribingHandler::FORMAT); + + $navigator = new SerializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $this->accessor, $this->dispatcher); + $navigator->initialize($this->serializationVisitor, $this->context); + $this->context->initialize(TestSubscribingHandler::FORMAT, $this->serializationVisitor, $navigator, $this->metadataFactory); + + $rt = $navigator->accept($object, null); + $this->assertEquals('foobar', $rt); + } + + /** + * @doesNotPerformAssertions + */ + public function testFilterableHandlerIsSkippedOnSerialization() + { + $object = new SerializableClass(); + $this->handlerRegistry->registerSubscribingHandler(new TestSkippableSubscribingHandler()); + + $this->context->method('getFormat')->willReturn(TestSkippableSubscribingHandler::FORMAT); + + $navigator = new SerializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $this->accessor, $this->dispatcher); + $navigator->initialize($this->serializationVisitor, $this->context); + $this->context->initialize(TestSkippableSubscribingHandler::FORMAT, $this->serializationVisitor, $navigator, $this->metadataFactory); + + $navigator->accept($object, null); + } + + public function testFilterableHandlerIsNotSkippedOnSerialization() + { + $object = new SerializableClass(); + $this->handlerRegistry->registerSubscribingHandler(new TestSkippableSubscribingHandler(false)); + + $this->context->method('getFormat')->willReturn(TestSkippableSubscribingHandler::FORMAT); + + $navigator = new SerializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $this->accessor, $this->dispatcher); + $navigator->initialize($this->serializationVisitor, $this->context); + $this->context->initialize(TestSkippableSubscribingHandler::FORMAT, $this->serializationVisitor, $navigator, $this->metadataFactory); + + $this->expectException(NotAcceptableException::class); + $this->expectExceptionMessage(TestSkippableSubscribingHandler::EX_MSG); + $navigator->accept($object, null); + } + + /** + * @doesNotPerformAssertions + */ + public function testNavigatorDoesNotCrashWhenObjectConstructorReturnsNull() + { + $objectConstructor = $this->getMockBuilder(ObjectConstructorInterface::class)->getMock(); + $objectConstructor->method('construct')->willReturn(null); + $navigator = new DeserializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $objectConstructor, $this->accessor, $this->dispatcher); + $navigator->initialize($this->deserializationVisitor, $this->deserializationContext); + + $navigator->accept(['id' => 1234], ['name' => SerializableClass::class]); + } + protected function setUp(): void { $this->deserializationVisitor = $this->getMockBuilder(DeserializationVisitorInterface::class)->getMock(); @@ -138,7 +232,7 @@ protected function setUp(): void $this->context = $this->getMockBuilder(SerializationContext::class) ->enableOriginalConstructor() - ->setMethodsExcept(['getExclusionStrategy']) + ->setMethodsExcept(['getExclusionStrategy', 'initialize', 'startVisiting', 'stopVisiting']) ->getMock(); $this->deserializationContext = $this->getMockBuilder(DeserializationContext::class) @@ -168,14 +262,56 @@ class SerializableClass class TestSubscribingHandler implements SubscribingHandlerInterface { + public const FORMAT = 'foo'; + public static function getSubscribingMethods() { - return [[ - 'type' => 'JsonSerializable', - 'format' => 'foo', - 'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION, - 'method' => 'serialize', - ], + return [ + [ + 'type' => SerializableClass::class, + 'format' => self::FORMAT, + 'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION, + 'method' => 'serialize', + ], ]; } + + public function serialize(VisitorInterface $visitor, $userData, array $type, Context $context) + { + return 'foobar'; + } +} + +class TestSkippableSubscribingHandler implements SubscribingHandlerInterface +{ + public const FORMAT = 'foo'; + public const EX_MSG = 'This method should be skipped!'; + + private $shouldSkip; + + public function __construct(bool $shouldSkip = true) + { + $this->shouldSkip = $shouldSkip; + } + + public static function getSubscribingMethods() + { + return [ + [ + 'type' => SerializableClass::class, + 'format' => self::FORMAT, + 'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION, + 'method' => 'serialize', + ], + ]; + } + + public function serialize(VisitorInterface $visitor, $userData, array $type, Context $context) + { + if ($this->shouldSkip) { + throw new SkipHandlerException(); + } + + throw new NotAcceptableException(self::EX_MSG); + } } diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index d1f95f4e0..fc37f3c5a 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -26,6 +26,7 @@ protected function getContent($key) static $outputs = []; if (!$outputs) { + $outputs['nullable_root'] = 'null'; $outputs['readonly'] = '{"id":123,"full_name":"Ruud Kamphuis"}'; $outputs['string'] = '"foo"'; $outputs['boolean_true'] = 'true'; @@ -93,6 +94,7 @@ protected function getContent($key) $outputs['object_when_null_and_serialized'] = '{"author":null,"text":"foo"}'; $outputs['date_time'] = '"2011-08-30T00:00:00+00:00"'; $outputs['date_time_immutable'] = '"2011-08-30T00:00:00+00:00"'; + $outputs['date_time_multi_format'] = '"2011-08-30T00:00:00+00:00"'; $outputs['timestamp'] = '{"timestamp":1455148800}'; $outputs['timestamp_prev'] = '{"timestamp":"1455148800"}'; $outputs['date_interval'] = '"PT45M"'; @@ -124,6 +126,10 @@ protected function getContent($key) $outputs['array_iterator'] = '{"iterator":{"foo":"bar","bar":"foo"}}'; $outputs['generator'] = '{"generator":{"foo":"bar","bar":"foo"}}'; $outputs['ParentNoMetadataChildObject'] = '{"bar":"John"}'; + $outputs['user_discriminator_array'] = '[{"entityName":"User"},{"entityName":"ExtendedUser"}]'; + $outputs['user_discriminator'] = '{"entityName":"User"}'; + $outputs['user_discriminator_extended'] = '{"entityName":"ExtendedUser"}'; + $outputs['typed_props'] = '{"id":1,"role":{"id":5},"vehicle":{"type":"car"},"created":"2010-10-01T00:00:00+00:00","updated":"2011-10-01T00:00:00+00:00","tags":["a","b"]}'; } if (!isset($outputs[$key])) { @@ -213,16 +219,14 @@ static function (SerializationVisitorInterface $visitor, AuthorList $data, array self::assertEquals('[{"full_name":"new name"},{"full_name":"new name"}]', $this->serialize($list)); } - /** - * @expectedException RuntimeException - * @expectedExceptionMessage Invalid data "baz" (string), expected "JMS\Serializer\Tests\Fixtures\Author". - */ public function testDeserializingObjectWithObjectPropertyWithNoArrayToObject() { $content = $this->getContent('object_with_object_property_no_array_to_author'); - $object = $this->deserialize($content, 'JMS\Serializer\Tests\Fixtures\ObjectWithObjectProperty'); - self::assertEquals('bar', $object->getFoo()); - self::assertInstanceOf('JMS\Serializer\Tests\Fixtures\Author', $object->getAuthor()); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Invalid data "baz" (string), expected "JMS\Serializer\Tests\Fixtures\Author".'); + + $this->deserialize($content, 'JMS\Serializer\Tests\Fixtures\ObjectWithObjectProperty'); } public function testDeserializingObjectWithObjectProperty() @@ -268,10 +272,7 @@ public function testPrimitiveTypes($primitiveType, $data) $visitor->setNavigator($navigator); $functionToCall = 'visit' . ucfirst($primitiveType); $result = $visitor->$functionToCall($data, [], $this->getMockBuilder(SerializationContext::class)->getMock()); - if ('double' === $primitiveType) { - $primitiveType = 'float'; - } - self::assertInternalType($primitiveType, $result); + self::{'assertIs' . (['boolean' => 'bool', 'integer' => 'int', 'double' => 'float'][$primitiveType] ?? $primitiveType)}($result); } /** @@ -284,23 +285,27 @@ public function testSerializeEmptyObject() /** * @group encoding - * @expectedException RuntimeException - * @expectedExceptionMessage Your data could not be encoded because it contains invalid UTF8 characters. */ public function testSerializeWithNonUtf8EncodingWhenDisplayErrorsOff() { ini_set('display_errors', '1'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Your data could not be encoded because it contains invalid UTF8 characters.'); + $this->serialize(['foo' => 'bar', 'bar' => pack('H*', 'c32e')]); } /** * @group encoding - * @expectedException RuntimeException - * @expectedExceptionMessage Your data could not be encoded because it contains invalid UTF8 characters. */ public function testSerializeWithNonUtf8EncodingWhenDisplayErrorsOn() { ini_set('display_errors', '0'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Your data could not be encoded because it contains invalid UTF8 characters.'); + $this->serialize(['foo' => 'bar', 'bar' => pack('H*', 'c32e')]); } diff --git a/tests/Serializer/Type/ParserTest.php b/tests/Serializer/Type/ParserTest.php index 9ceec0d6d..0b1b0510c 100644 --- a/tests/Serializer/Type/ParserTest.php +++ b/tests/Serializer/Type/ParserTest.php @@ -43,130 +43,128 @@ public function validTypesProvider(): iterable 'string', $type('string'), ]; + yield [ 'array', $type('array', [['name' => 'Foo', 'params' => []]]), ]; + yield [ 'Foo<\'a\'>', $type('Foo', ['a']), ]; + + yield [ + 'Foo<>', + $type('Foo', []), + ]; + yield [ 'Foo<5>', $type('Foo', [5]), ]; + yield [ 'Foo<5.5>', $type('Foo', [5.5]), ]; + yield [ 'Foo', $type('Foo', [null]), ]; + yield [ 'Foo<\'a\',\'b\',\'c\'>', $type('Foo', ['a', 'b', 'c']), ]; + yield [ 'Foo<\'a\',\'\'>', $type('Foo', ['a', '']), ]; + yield [ 'array', $type('array', [['name' => 'Foo', 'params' => []], ['name' => 'Bar', 'params' => []]]), ]; + yield [ 'array', $type('array', [['name' => 'Foo\Bar', 'params' => []], ['name' => 'Baz\Boo', 'params' => []]]), ]; + yield [ 'a,e>', $type('a', [['name' => 'b', 'params' => [['name' => 'c', 'params' => []], ['name' => 'd', 'params' => []]]], ['name' => 'e', 'params' => []]]), ]; + yield [ 'Foo', $type('Foo'), ]; + yield [ 'Foo\Bar', $type('Foo\Bar'), ]; + yield [ 'Foo<"asdf asdf">', $type('Foo', ['asdf asdf']), ]; - } - - public function testEmptyString(): void - { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage( - "Unexpected token \"EOF\" (EOF) at line 1 and column 1:\n" - . "\n" - . '↑' - ); - $this->parser->parse(''); - } - - public function testParamTypeMustEndWithBracket(): void - { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage( - "Unexpected token \"EOF\" (EOF) at line 1 and column 8:\n" - . "Foo', + $type('Foo', [[]]), + ]; - $this->parser->parse('Foo', + $type('Foo', [[[]]]), + ]; - public function testMustStartWithName(): void - { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage( - "Unexpected token \",\" (comma) at line 1 and column 1:\n" - . ",\n" - . '↑' - ); + yield [ + 'Foo<[123]>', + $type('Foo', [[123]]), + ]; - $this->parser->parse(','); - } + yield [ + 'Foo<[123, 456]>', + $type('Foo', [[123, 456]]), + ]; - public function testEmptyParams(): void - { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage( - "Unexpected token \">\" (_parenthesis) at line 1 and column 5:\n" - . "Foo<>\n" - . ' ↑' - ); + yield [ + 'Foo<[[123], 456, "bar"]>', + $type('Foo', [[[123], 456, 'bar']]), + ]; - $this->parser->parse('Foo<>'); + yield [ + 'DateTime', + $type('DateTime', [null, null, ['Y-m-d\TH:i:s', 'Y-m-d\TH:i:sP']]), + ]; } - public function testNoTrailingComma(): void + /** + * @dataProvider wrongSyntax + */ + public function testSyntaxError($value): void { $this->expectException(SyntaxError::class); - $this->expectExceptionMessage( - "Unexpected token \",\" (comma) at line 1 and column 7:\n" - . "Foo\n" - . ' ↑' - ); - - $this->parser->parse('Foo'); + $this->parser->parse($value); } - public function testLeadingBackslash(): void + public function wrongSyntax() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage( - "Unrecognized token \"\\\" at line 1 and column 5:\n" - . "Foo<\Bar>\n" - . ' ↑' - ); - - $this->parser->parse('Foo<\Bar>'); + return [ + ['Foo<\Bar>]'], + ['Foo'], + ['Foo'], + ]; } } diff --git a/tests/Serializer/XmlSerializationTest.php b/tests/Serializer/XmlSerializationTest.php index 6fb3b9659..da8b0a389 100644 --- a/tests/Serializer/XmlSerializationTest.php +++ b/tests/Serializer/XmlSerializationTest.php @@ -7,6 +7,8 @@ use JMS\Serializer\Context; use JMS\Serializer\DeserializationContext; use JMS\Serializer\Exception\InvalidArgumentException; +use JMS\Serializer\Exception\RuntimeException; +use JMS\Serializer\Exception\XmlErrorException; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\Handler\DateHandler; use JMS\Serializer\Handler\HandlerRegistryInterface; @@ -46,12 +48,12 @@ class XmlSerializationTest extends BaseSerializationTest { - /** - * @expectedException \JMS\Serializer\Exception\RuntimeException - */ public function testInvalidUsageOfXmlValue() { $obj = new InvalidUsageOfXmlValue(); + + $this->expectException(RuntimeException::class); + $this->serialize($obj); } @@ -72,7 +74,6 @@ public function getXMLBooleans() public function testAccessorSetterDeserialization() { - /** @var AccessorSetter $object */ $object = $this->deserialize( ' @@ -83,6 +84,7 @@ public function testAccessorSetterDeserialization() ', 'JMS\Serializer\Tests\Fixtures\AccessorSetter' ); + \assert($object instanceof AccessorSetter); self::assertInstanceOf('stdClass', $object->getElement()); self::assertInstanceOf('JMS\Serializer\Tests\Fixtures\AccessorSetterElement', $object->getElement()->element); @@ -115,12 +117,11 @@ public function testPropertyIsCollectionOfObjectsWithAttributeAndValue() self::assertEquals($this->getContent('person_collection'), $this->serialize($personCollection)); } - /** - * @expectedException JMS\Serializer\Exception\InvalidArgumentException - * @expectedExceptionMessage The document type "]>" is not allowed. If it is safe, you may add it to the whitelist configuration. - */ public function testExternalEntitiesAreDisabledByDefault() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The document type "]>" is not allowed. If it is safe, you may add it to the whitelist configuration.'); + $this->deserialize(' @@ -130,12 +131,11 @@ public function testExternalEntitiesAreDisabledByDefault() ', 'stdClass'); } - /** - * @expectedException JMS\Serializer\Exception\InvalidArgumentException - * @expectedExceptionMessage The document type "" is not allowed. If it is safe, you may add it to the whitelist configuration. - */ public function testDocumentTypesAreNotAllowed() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The document type "" is not allowed. If it is safe, you may add it to the whitelist configuration.'); + $this->deserialize('', 'stdClass'); } @@ -327,14 +327,13 @@ public function testDateTimeImmutableNoCData($key, $value, $type) self::assertEquals($this->getContent($key . '_no_cdata'), $serializer->serialize($value, $this->getFormat())); } - /** - * @expectedException JMS\Serializer\Exception\RuntimeException - * @expectedExceptionMessage Unsupported value type for XML attribute map. Expected array but got object - */ public function testXmlAttributeMapWithoutArray() { $attributes = new \ArrayObject(['type' => 'text']); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Unsupported value type for XML attribute map. Expected array but got object'); + $this->serializer->serialize(new Input($attributes), $this->getFormat()); } @@ -389,10 +388,10 @@ public function testObjectWithXmlNamespaces() $deserialized = $this->deserialize($this->getContent('object_with_xml_namespacesalias'), get_class($object)); self::assertEquals('2011-07-30T00:00:00+00:00', $this->getField($deserialized, 'createdAt')->format(\DateTime::ATOM)); - self::assertAttributeEquals('This is a nice title.', 'title', $deserialized); - self::assertAttributeSame('e86ce85cdb1253e4fc6352f5cf297248bceec62b', 'etag', $deserialized); - self::assertAttributeSame('en', 'language', $deserialized); - self::assertAttributeEquals('Foo Bar', 'author', $deserialized); + self::assertSame('This is a nice title.', $this->getField($deserialized, 'title')); + self::assertSame('e86ce85cdb1253e4fc6352f5cf297248bceec62b', $this->getField($deserialized, 'etag')); + self::assertSame('en', $this->getField($deserialized, 'language')); + self::assertSame('Foo Bar', $this->getField($deserialized, 'author')); self::assertEquals('value for empty namespace property', $this->getField($deserialized, 'emptyNsElement')); } @@ -527,11 +526,10 @@ public function testDiscriminatorAsXmlAttributeWithNamespace() ); } - /** - * @expectedException \JMS\Serializer\Exception\XmlErrorException - */ public function testDeserializeEmptyString() { + $this->expectException(XmlErrorException::class); + $this->deserialize('', 'stdClass'); } @@ -574,6 +572,7 @@ public function testDoubleEncoding() private function xpathFirstToString(\SimpleXMLElement $xml, $xpath) { $nodes = $xml->xpath($xpath); + return (string) reset($nodes); } diff --git a/tests/Serializer/xml/date_time_multi_format.xml b/tests/Serializer/xml/date_time_multi_format.xml new file mode 100644 index 000000000..5d0767627 --- /dev/null +++ b/tests/Serializer/xml/date_time_multi_format.xml @@ -0,0 +1,2 @@ + + diff --git a/tests/Serializer/xml/date_time_multi_format_no_cdata.xml b/tests/Serializer/xml/date_time_multi_format_no_cdata.xml new file mode 100644 index 000000000..d596a0f83 --- /dev/null +++ b/tests/Serializer/xml/date_time_multi_format_no_cdata.xml @@ -0,0 +1,2 @@ + +2011-08-30T00:00:00+00:00 diff --git a/tests/Serializer/xml/nullable_root.xml b/tests/Serializer/xml/nullable_root.xml new file mode 100644 index 000000000..36766ecd6 --- /dev/null +++ b/tests/Serializer/xml/nullable_root.xml @@ -0,0 +1,2 @@ + + diff --git a/tests/Serializer/xml/typed_props.xml b/tests/Serializer/xml/typed_props.xml new file mode 100644 index 000000000..f6c3b9feb --- /dev/null +++ b/tests/Serializer/xml/typed_props.xml @@ -0,0 +1,16 @@ + + + 1 + + 5 + + + + + + + + + + + diff --git a/tests/Serializer/xml/user_discriminator.xml b/tests/Serializer/xml/user_discriminator.xml new file mode 100644 index 000000000..b93db3067 --- /dev/null +++ b/tests/Serializer/xml/user_discriminator.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/Serializer/xml/user_discriminator_array.xml b/tests/Serializer/xml/user_discriminator_array.xml new file mode 100644 index 000000000..135e92453 --- /dev/null +++ b/tests/Serializer/xml/user_discriminator_array.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/Serializer/xml/user_discriminator_extended.xml b/tests/Serializer/xml/user_discriminator_extended.xml new file mode 100644 index 000000000..269282c6d --- /dev/null +++ b/tests/Serializer/xml/user_discriminator_extended.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/SerializerBuilderTest.php b/tests/SerializerBuilderTest.php index fbad6e152..6bd287ece 100644 --- a/tests/SerializerBuilderTest.php +++ b/tests/SerializerBuilderTest.php @@ -5,14 +5,19 @@ namespace JMS\Serializer\Tests; use JMS\Serializer\DeserializationContext; +use JMS\Serializer\Exception\UnsupportedFormatException; use JMS\Serializer\Expression\ExpressionEvaluator; use JMS\Serializer\Handler\HandlerRegistry; use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializerBuilder; +use JMS\Serializer\Tests\Fixtures\DocBlockType\Collection\Details\ProductDescription; +use JMS\Serializer\Tests\Fixtures\DocBlockType\SingleClassFromDifferentNamespaceTypeHint; use JMS\Serializer\Tests\Fixtures\PersonSecret; use JMS\Serializer\Tests\Fixtures\PersonSecretWithVariables; use JMS\Serializer\Type\ParserInterface; use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; +use PHPUnit\Framework\Constraint\FileExists; +use PHPUnit\Framework\Constraint\LogicalNot; use PHPUnit\Framework\TestCase; use Symfony\Component\ExpressionLanguage\ExpressionFunction; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; @@ -40,7 +45,8 @@ public function testBuildWithoutAnythingElse() public function testWithCache() { - self::assertFileNotExists($this->tmpDir); + // @todo change to static::assertFileNotExists when support for PHPUnit 8 and PHP 7.2 is dropped + static::assertThat($this->tmpDir, new LogicalNot(new FileExists())); self::assertSame($this->builder, $this->builder->setCacheDir($this->tmpDir)); $serializer = $this->builder->build(); @@ -50,8 +56,8 @@ public function testWithCache() self::assertFileExists($this->tmpDir . '/metadata'); $factory = $this->getField($serializer, 'factory'); - self::assertAttributeSame(false, 'debug', $factory); - self::assertAttributeNotSame(null, 'cache', $factory); + self::assertFalse($this->getField($factory, 'debug')); + self::assertNotNull($this->getField($factory, 'cache')); } public function testDoesAddDefaultHandlers() @@ -88,10 +94,6 @@ public function testDoesNotAddDefaultHandlersWhenExplicitlyConfigured() self::assertEquals('{}', $this->builder->build()->serialize(new \DateTime('2020-04-16'), 'json')); } - /** - * @expectedException JMS\Serializer\Exception\UnsupportedFormatException - * @expectedExceptionMessage The format "xml" is not supported for serialization. - */ public function testDoesNotAddOtherVisitorsWhenConfiguredExplicitly() { self::assertSame( @@ -99,6 +101,9 @@ public function testDoesNotAddOtherVisitorsWhenConfiguredExplicitly() $this->builder->setSerializationVisitor('json', new JsonSerializationVisitorFactory()) ); + $this->expectException(UnsupportedFormatException::class); + $this->expectExceptionMessage('The format "xml" is not supported for serialization.'); + $this->builder->build()->serialize('foo', 'xml'); } @@ -254,6 +259,26 @@ public function testExpressionEngineWhenDeserializing() self::assertEquals($person, $object); } + public function testEnablingDocBlockResolver() + { + $language = new ExpressionLanguage(); + $this->builder->setExpressionEvaluator(new ExpressionEvaluator($language)); + $this->builder->setDocBlockTypeResolver(true); + + $serializer = $this->builder->build(); + + $person = new SingleClassFromDifferentNamespaceTypeHint(); + $productDescription = new ProductDescription(); + $productDescription->description = 'info'; + $person->data = $productDescription; + + $serialized = $serializer->serialize($person, 'json'); + self::assertEquals('{"data":{"description":"info"}}', $serialized); + + $object = $serializer->deserialize($serialized, SingleClassFromDifferentNamespaceTypeHint::class, 'json'); + self::assertEquals($person, $object); + } + protected function setUp(): void { $this->builder = SerializerBuilder::create();