├── .github
└── workflows
│ ├── ci.yml
│ └── static.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
├── composer.json
├── phpstan.neon
├── phpstan.tests.neon
├── phpunit.xml.dist
├── src
├── Annotation
│ └── Preferred.php
├── Builder.php
├── Exception
│ ├── InvalidTypeException.php
│ ├── ParseException.php
│ ├── RecursionException.php
│ └── SchemaException.php
├── Metadata
│ ├── AbstractPropertyMetadata.php
│ ├── AbstractPropertyType.php
│ ├── ClassMetadata.php
│ ├── DateTimeOptions.php
│ ├── ParameterMetadata.php
│ ├── PropertyAccessor.php
│ ├── PropertyMetadata.php
│ ├── PropertyType.php
│ ├── PropertyTypeArray.php
│ ├── PropertyTypeClass.php
│ ├── PropertyTypeDateTime.php
│ ├── PropertyTypeIterable.php
│ ├── PropertyTypePrimitive.php
│ ├── PropertyTypeUnknown.php
│ └── VersionRange.php
├── ModelParser
│ ├── JMSParser.php
│ ├── JMSParserLegacy.php
│ ├── LiipMetadataAnnotationParser.php
│ ├── ModelParserInterface.php
│ ├── ParserContext.php
│ ├── PhpDocParser.php
│ ├── RawMetadata
│ │ ├── PropertyCollection.php
│ │ ├── PropertyVariationMetadata.php
│ │ └── RawClassMetadata.php
│ ├── ReflectionParser.php
│ └── VisibilityAwarePropertyAccessGuesser.php
├── Parser.php
├── PropertyReducer.php
├── RawClassMetadataRegistry.php
├── RecursionChecker.php
├── RecursionContext.php
├── Reducer
│ ├── GroupReducer.php
│ ├── PreferredReducer.php
│ ├── PropertyReducerInterface.php
│ ├── TakeBestReducer.php
│ └── VersionReducer.php
└── TypeParser
│ ├── JMSTypeParser.php
│ └── PhpTypeParser.php
└── tests
├── BuilderTest.php
├── Metadata
├── ClassMetadataTest.php
├── PropertyTypeIterableTest.php
├── PropertyTypeTest.php
└── VersionRangeTest.php
├── ModelParser
├── Fixtures
│ ├── IntersectionTypeDeclarationModel.php
│ ├── TypeDeclarationModel.php
│ └── UnionTypeDeclarationModel.php
├── JMSParserTest81.php
├── JMSParserTestCase.php
├── JMSParserTestLegacy.php
├── Model
│ ├── AbstractModel.php
│ ├── BaseModel.php
│ ├── Nested.php
│ ├── Recursion.php
│ ├── ReflectionAbstractModel.php
│ ├── ReflectionBaseModel.php
│ └── WithImports.php
├── ParserContextTest.php
├── PhpDocParserTest.php
├── RawMetadata
│ └── RawClassMetadataTest.php
├── ReflectionParserTest.php
└── VisibilityAwarePropertyAccessGuesserTest.php
├── ParserTest.php
├── PropertyReducerTest.php
├── RecursionCheckerTest.php
├── RecursionContextTest.php
├── Reducer
├── GroupReducerTest.php
├── TakeBestReducerTest.php
└── VersionReducerTest.php
├── TypeParser
├── JMSTypeParserTest.php
└── PhpTypeParserTest.php
└── bootstrap.php
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*.x'
7 | tags:
8 | - '[0-9].[0-9]+'
9 | pull_request:
10 |
11 | jobs:
12 | test:
13 | name: "PHPUnit"
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | php-version: ['7.4', '8.0', '8.1', '8.2']
18 | include:
19 | - php-version: '7.4'
20 | composer-flags: '--prefer-stable --prefer-lowest'
21 | steps:
22 | - name: Check out code into the workspace
23 | uses: actions/checkout@v3
24 | - name: Setup PHP ${{ matrix.php-version }}
25 | uses: shivammathur/setup-php@v2
26 | with:
27 | php-version: ${{ matrix.php-version }}
28 | - name: Remove dev tools to not interfere with dependencies
29 | run: composer remove --dev friendsofphp/php-cs-fixer phpstan/phpstan-phpunit phpstan/phpstan
30 | - name: Composer cache
31 | uses: actions/cache@v3
32 | with:
33 | path: ${{ env.HOME }}/.composer/cache
34 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
35 | - name: Install dependencies
36 | run: composer update ${{ matrix.composer-flags }} --prefer-dist --no-interaction
37 | - name: Run tests
38 | run: composer phpunit
39 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | name: Static
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*.x'
7 | tags:
8 | - '[0-9].[0-9]+'
9 | pull_request:
10 |
11 | jobs:
12 | phpstan:
13 | name: "PHPStan"
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Check out code into the workspace
17 | uses: actions/checkout@v3
18 | - name: Setup PHP 8.2
19 | uses: shivammathur/setup-php@v2
20 | with:
21 | php-version: 8.2
22 | - name: Composer cache
23 | uses: actions/cache@v3
24 | with:
25 | path: ${{ env.HOME }}/.composer/cache
26 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
27 | - name: Install dependencies
28 | run: composer update --prefer-dist --no-interaction
29 | - name: PHPStan
30 | run: composer phpstan-all
31 |
32 | cs:
33 | name: "CS Fixer"
34 | runs-on: ubuntu-latest
35 | steps:
36 | - name: Check out code into the workspace
37 | uses: actions/checkout@v3
38 | - name: Setup PHP 8.2
39 | uses: shivammathur/setup-php@v2
40 | with:
41 | php-version: 8.2
42 | - name: Validate composer.json
43 | run: composer validate --strict --no-check-lock
44 | - name: Composer cache
45 | uses: actions/cache@v3
46 | with:
47 | path: ${{ env.HOME }}/.composer/cache
48 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
49 | - name: Install dependencies
50 | run: composer update --prefer-dist --no-interaction
51 | - name: CS Fixer
52 | run: composer cs-fixer
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Composer files
2 | /vendor
3 | /composer.lock
4 |
5 | ###> phpunit/phpunit ###
6 | /phpunit.xml
7 | /.phpunit.result.cache
8 | ###< phpunit/phpunit ###
9 |
10 | ###> friendsofphp/php-cs-fixer ###
11 | /.php-cs-fixer.php
12 | /.php-cs-fixer.cache
13 | ###< friendsofphp/php-cs-fixer ###
14 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | setFinder(
8 | PhpCsFixer\Finder::create()
9 | ->in([
10 | __DIR__,
11 | ])
12 | ->notPath('src/ModelParser/JMSParserLegacy.php')
13 | );
14 |
15 | $config
16 | ->setRiskyAllowed(true)
17 | ->setRules(
18 | [
19 | '@PhpCsFixer' => true,
20 | '@PhpCsFixer:risky' => true,
21 | '@Symfony' => true,
22 | '@Symfony:risky' => true,
23 | 'array_syntax' => ['syntax' => 'short'],
24 | 'declare_strict_types' => true,
25 | 'list_syntax' => ['syntax' => 'short'],
26 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
27 | 'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'],
28 | 'native_function_invocation' => [
29 | 'include' => ['@compiler_optimized'],
30 | ],
31 | 'no_superfluous_phpdoc_tags' => true,
32 | 'php_unit_dedicate_assert' => true,
33 | 'php_unit_expectation' => true,
34 | 'php_unit_mock' => true,
35 | 'php_unit_namespaced' => true,
36 | 'php_unit_no_expectation_annotation' => true,
37 | 'phpdoc_to_return_type' => true,
38 | 'static_lambda' => true,
39 | 'ternary_to_null_coalescing' => true,
40 | 'void_return' => true,
41 |
42 | // Don't mark tests as @internal
43 | 'php_unit_internal_class' => false,
44 |
45 | // Don't require @covers in tests
46 | 'php_unit_test_class_requires_covers' => false,
47 |
48 | // Don't require dots in phpdocs
49 | 'phpdoc_annotation_without_dot' => false,
50 | 'phpdoc_summary' => false,
51 |
52 | // Sometimes we need to do non-strict comparison
53 | 'strict_comparison' => false,
54 |
55 | // The convention with phpunit has been to use assertions with the object context.
56 | 'php_unit_test_case_static_method_calls' => false,
57 |
58 | // Not supported in PHP 7
59 | 'get_class_to_class_keyword' => false,
60 | 'modernize_strpos' => false,
61 | ]
62 | )
63 | ;
64 |
65 | return $config;
66 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | # Version 1.x
4 |
5 | # 1.2.0
6 |
7 | * DateTimeOptions now features a list of deserialization formats instead of a single string one. Passing a string instead of an array to its `__construct`or is deprecated, and will be forbidden in the next version
8 | Similarly, `getDeserializeFormat(): ?string` is deprecated in favor of `getDeserializeFormats(): ?array`
9 | * Added `PropertyTypeIterable`, which generalizes `PropertyTypeArray` to allow merging Collection informations like one would with arrays, including between interfaces and concrete classes
10 | * Deprecated `PropertyTypeArray`, please prefer using `PropertyTypeIterable` instead
11 | * `PropertyTypeArray::isCollection()` and `PropertyTypeArray::getCollectionClass()` are deprecated, including in its child classes, in favor of `isTraversable()` and `getTraversableClass()`
12 | * Added a model parser `VisibilityAwarePropertyAccessGuesser` that tries to guess getter and setter methods for non-public properties.
13 |
14 | # 1.1.0
15 |
16 | * Drop support for PHP 7.2 and PHP 7.3
17 | * Support doctrine annotations `2.x`
18 |
19 | # 1.0.0
20 |
21 | No changes since 0.6.1.
22 |
23 | # 0.6.1
24 |
25 | * Do not ignore methods that have no phpdoc but do have a PHP 8.1 attribute to make them virtual properties.
26 |
27 | # 0.6.0
28 |
29 | * When running with PHP 8, process attributes in addition to the phpdoc annotations.
30 | * Support doctrine collections
31 | * Support `identical` property naming strategy
32 | * Add support for the `MaxDepth` annotation from JMS
33 |
34 | # 0.5.0
35 |
36 | * Support JMS Serializer `ReadOnlyProperty` in addition to `ReadOnly` to be compatible with serializer 3.14 and newer.
37 | * Support PHP 8.1 (which makes ReadOnly a reserved keyword)
38 |
39 | # 0.4.1
40 |
41 | * Allow installation with psr/log 2 and 3, to allow installation with Symfony 6
42 |
43 | # 0.4.0
44 |
45 | * Handle property type declarations in reflection parser.
46 | * [Bugfix] Upgrade array type with `@var Type[]` annotation
47 | * [Bugfix] When extending class redefines a property, use phpdoc from extending class rather than base class
48 | * [Bugfix] Use correct context for relative class names in inherited properties/methods
49 |
50 | # 0.3.0
51 |
52 | * Support PHP 8, drop support for PHP 7.1
53 | * Support JMS 3 and drop support for JMS 1
54 | * [BC Break] Only if you wrote your own PropertyMetadata class and overwrote `getCustomInformation`: That method now has the return type `:mixed` specified.
55 |
56 | # 0.2.1
57 |
58 | * [Bugfix] Look in parent classes and traits for imports
59 |
60 | # 0.2.0
61 |
62 | * [Feature] Support to track custom metadata
63 |
64 | # 0.1.0
65 |
66 | * Initial release
67 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
2 |
3 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
4 |
5 | Examples of unacceptable behavior by participants include:
6 | * The use of sexualized language or imagery
7 | * Personal attacks
8 | * Trolling or insulting/derogatory comments
9 | * Public or private harassment
10 | * Publishing other’s private information, such as physical or electronic addresses, without explicit permission
11 | * Other unethical or unprofessional conduct.
12 |
13 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
14 |
15 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
16 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
17 |
18 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org/) , version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
19 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | If you wish to contribute to Metadata-Parser, feel free to do so.
2 |
3 | It is generally a good idea to open an issue before you create a Pull-Request to avoid wasting your time. That way the maintainers can ensure that the changes you want to make, is something that the project would welcome.
4 |
5 | ## What to work on
6 | You can work on anything! All the current issues are listed on the GitHub issue tracker. If you don’t see the thing you want to work on there, please open a new issue.
7 |
8 | PR’s improving documentation and tests are always very welcome too.
9 |
10 | ## Pull-Request workflow
11 | Change the code in your own fork/branch. When you believe the code is ready to be applied, submit the PR.
12 |
13 | Once you have submitted the PR, people will look at it and provide feedback.
14 |
15 | If it cannot be applied in its current state, then a comment will be left and the PR will be closed.
16 |
17 | In general, only submit a PR if you believe it can be applied in its current state. If you need an exception to this rule please use “WIP: “ in front of your PR to indicate that it is work in progress. This could be beneficial for instance if you’re looking for feedback while your PR is still in progress.
18 |
19 | When you have time to address the comments left on your PR, please make the changes, push them to github and then re-open the pull request.
20 |
21 | If you do not have time to address the changes, that’s OK. Just leave a comment in the PR and either someone else can pick up the changes, or the PR will be closed.
22 |
23 | ## Coding Style & Decisions
24 | We write tests for our code, if you add code, please also add tests for that code. We do not demand specific code coverage, but we want tests where they make sense.
25 |
26 | In general this project uses the [Symfony coding standards](https://symfony.com/doc/current/contributing/co).
27 |
28 | We have our own standards when it comes to PHPDoc, when in doubt use these rules over Symfony’s.
29 |
30 | ### PHPDoc
31 | It’s a good idea to mention GitHub issue numbers when there are hardcoded special cases or other business decisions in the code. Always aim to explain the why behind the how;
32 |
33 | If there is something to explain about a class, use the class docblock and not the constructor. Constructors will rarely need explanation apart from parameter descriptions;
34 |
35 | Use the class docblock to explain both technical but also business WHY questions;
36 |
37 | Add the @var Type annotation on all properties of the class. The type is usually also clearly defined from the constructor and the assignment, but we find it more readable with the @var annotations;
38 |
39 | Do not use the @throws tag unless you have something very specific to say about it. We do not care to track exceptions flow through all the code. Disable the warning for this in your IDE if needed;
40 |
41 | Inline comments should be kept when they add value (for example reference GitHub issues, explain a regular expression, etc). But when the code is hard to read, consider refactoring by extracting code into private methods or separate classes. With good naming, such methods can often replace a code comment.
42 |
43 | When in doubt, add a comment.
44 |
45 | #### Parameter documentation
46 | Parameter documentation should explain the value range if there are restrictions further than the type. Use @param annotations if:
47 |
48 | * There is further information about the value range that is not obvious from the type. E.g. a string that can only be some specific constants, or @param int $priority From 0 to 10. The higher the number, the higher the priority. One special case is arrays, which can not be further declared in PHP;
49 |
50 | * Always specify the list element types of arrays with a docblock, e.g. @param string[] $list;
51 |
52 | * Only add documentation for parameters where there is something to add. "Incomplete" doc blocks are fine. If your IDE is complaining about this: Disable the warnings for docblocks. We have code sniffers to ensure our doc blocks do not contain wrong parameter names.
53 |
54 | #### Return type documentation
55 | Use the : Type syntax, resp : ?Type for Type|null. Do not use @return unless an array is returned, or when mixed types are returned.
56 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | Metadata-Parser is the result of the work of many people who made the code, the documentation and anything around it better.
2 |
3 | Thank you for your work.
4 |
5 | These are the people who in any way contributed to the project (in no particular order):
6 | David Buchmann (dbu)
7 | Michelle Sanver (michellesanver)
8 | Rae Knowler (bellisk)
9 | Tobias Schultze (Tobion)
10 | Christian Riesen (ChristianRiesen)
11 | Martin Sanser (mjanser)
12 | Emanuele Panzeri (thePanz)
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Liip
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Liip Metadata Parser
2 |
3 | **This project is Open Sourced based on work that we did initially as closed source at Liip, it may be lacking some documentation. If there is anything that you need or have questions about we would love to see you open an issue! :)**
4 |
5 | This is a parser for building model metadata from PHP classes. The metadata model can
6 | then be used to generate code or configuration. For example a serializer or
7 | ElasticSearch schema for types.
8 |
9 | The metadata is geared toward this use case. PHP level constructs that
10 | represent the same information are grouped together: Methods for virtual
11 | properties and fields that have the same serialized name, but are valid in
12 | different versions.
13 |
14 | This extensible parser can process PHP code and annotations or other metadata.
15 | You could write your own parsers, but this library comes with support for:
16 |
17 | * Reflection
18 | * PhpDoc
19 | * JMSSerializer annotations
20 |
21 | ## Contributing
22 |
23 | If you want to contribute to the project (awesome!!), please read the
24 | [Contributing Guidelines](https://github.com/liip/metadata-parser/blob/master/CONTRIBUTING.md)
25 | and adhere to our [Code Of Conduct](https://github.com/liip/metadata-parser/blob/master/CODE_OF_CONDUCT.md)
26 |
27 | ## Where do I go for help?
28 | If you need, open an issue.
29 |
30 | ## Setup
31 | ```php
32 | use Doctrine\Common\Annotations\AnnotationReader;
33 | use Liip\MetadataParser\Builder;
34 | use Liip\MetadataParser\Parser;
35 | use Liip\MetadataParser\RecursionChecker;
36 | use Liip\MetadataParser\ModelParser\JMSParser;
37 | use Liip\MetadataParser\ModelParser\LiipMetadataAnnotationParser;
38 | use Liip\MetadataParser\ModelParser\PhpDocParser;
39 | use Liip\MetadataParser\ModelParser\ReflectionParser;
40 | use Liip\MetadataParser\ModelParser\VisibilityAwarePropertyAccessGuesser;
41 |
42 | $parser = new Parser(
43 | new ReflectionParser(),
44 | new PhpDocParser(),
45 | new JMSParser(new AnnotationReader()),
46 | new VisibilityAwarePropertyAccessGuesser(),
47 | new LiipMetadataAnnotationParser(new AnnotationReader()),
48 | );
49 |
50 | $recursionChecker = new RecursionChecker(new NullLogger());
51 |
52 | $builder = new Builder($parser, $recursionChecker);
53 | ```
54 |
55 | ## Usage
56 |
57 | The `Builder::build` method is the main entry point to get a `ClassMetadata`
58 | object. The builder accepts an array of `Reducer` to select which property to
59 | use when there are several options. A reducer can lead to drop a property if
60 | none of the variants is acceptable. Multiple options for a single field mainly
61 | come from the `@SerializedName` and `@VirtualProperty` annotations of
62 | JMSSerializer:
63 |
64 | * GroupReducer: Select the property based on whether it is in any of the
65 | specified groups;
66 | * VersionReducer: Select the property based on whether it is included in the
67 | specified version;
68 | * TakeBestReducer: Make sure that we end up with the property that has the same
69 | name as the serialized name, if we still have multiple options after the
70 | other reducers.
71 |
72 | ```php
73 | use Liip\MetadataParser\Reducer\GroupReducer;
74 | use Liip\MetadataParser\Reducer\PreferredReducer;
75 | use Liip\MetadataParser\Reducer\TakeBestReducer;
76 | use Liip\MetadataParser\Reducer\VersionReducer;
77 |
78 | $reducers = [
79 | new VersionReducer('2'),
80 | new GroupReducer(['api', 'detail']),
81 | new PreferredReducer(),
82 | new TakeBestReducer(),
83 | ];
84 | $metadata = $builder->build(MyClass::class, $reducers);
85 | ```
86 |
87 | The `ClassMetadata` provides all information on a class. Properties have a
88 | `PropertyType` to tell what kind of property they are. Properties that hold
89 | another class, are of the type `PropertyTypeClass` that has the method
90 | `getClassMetadata()` to get the metadata of the nested class. This structure
91 | is validated to not contain any infinite recursion.
92 |
93 | ### Property naming strategy
94 |
95 | By default, property names will be translated from a camelCased to a lower and
96 | snake_cased name (e.g. `myProperty` becomes `my_property`). If you want to keep
97 | the property name as is, you can change the strategy to `identical` via the
98 | following code:
99 |
100 | ```php
101 | \Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection::useIdenticalNamingStrategy();
102 | ```
103 |
104 | ### Handling Edge Cases with @Preferred
105 |
106 | This library provides its own annotation in `Liip\MetadataParser\Annotation\Preferred`
107 | to specify which property to use in case there are several options. This can be
108 | useful for example when serializing models without specifying a version, when
109 | they use different virtual properties in different versions.
110 |
111 | ```php
112 | use JMS\Serializer\Annotation as JMS;
113 | use Liip\MetadataParser\Annotation as Liip;
114 |
115 | class Product
116 | {
117 | /**
118 | * @JMS\Since("2")
119 | * @JMS\Type("string")
120 | */
121 | public $name;
122 |
123 | /**
124 | * @JMS\Until("1")
125 | * @JMS\SerializedName("name")
126 | * @JMS\Type("string")
127 | * @Liip\Preferred
128 | */
129 | public $legacyName;
130 | }
131 | ```
132 |
133 | ### Expected Recursion: Working with Flawed Models
134 |
135 | #### JMS annotation
136 |
137 | If you are using JMS annotations, you can add the `MaxDepth` annotation to
138 | properties that might be recursive.
139 |
140 | The following example will tell the metadata parser that the recursion is
141 | expected up to a maximum depth of `3`.
142 |
143 | ```php
144 | use JMS\Serializer\Annotation as JMS;
145 |
146 | class RecursionModel
147 | {
148 | /**
149 | * @JMS\MaxDepth(3)
150 | * @JMS\Type("RecursionModel")
151 | */
152 | public $recursion;
153 | }
154 | ```
155 |
156 | #### Pure PHP
157 |
158 | The RecursionChecker accepts a second parameter to specify places where to
159 | break recursion. This is useful if your model tree looks like it has recursions
160 | but actually does not have them. JMSSerializer always acts on the actual data
161 | and therefore does not notice a recursion as long as it is not infinite.
162 |
163 | For example, lets say you have a `Product` that has a field `variants` which is
164 | again list of `Product`. Those variant products use the same class and
165 | therefore have the `variants` field. However, in real data a variant never
166 | contains further variants. To avoid a recursion exception for this example, you
167 | would specify:
168 |
169 | ```php
170 | $expectedRecursions = [
171 | ['variants', 'variants'],
172 | ];
173 | $recursionChecker = new RecursionChecker(new NullLogger(), $expectedRecursions);
174 | ```
175 |
176 | With this configuration, the `ClassMetadata` found in the property type for the
177 | variants property of the final model will have no field `variants`, so that
178 | code working on the metadata does not need to worry about infinite recursion.
179 |
180 | ## Extending the metadata parser
181 |
182 | This library comes with a couple of parsers, but you can write your own to
183 | handle custom information specific to your project. Use the
184 | `PropertyVariationMetadata::setCustomInformation` method to add custom data,
185 | and use `PropertyMetadata::getCustomInformation` to read it in your metadata
186 | consumers.
187 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "liip/metadata-parser",
3 | "description": "Parser for metadata",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Liip AG",
9 | "homepage": "http://www.liip.ch"
10 | }
11 | ],
12 | "support": {
13 | "source": "https://github.com/liip/metadata-parser",
14 | "issues": "https://github.com/liip/metadata-parser/issues"
15 | },
16 | "require": {
17 | "php": "^7.4 || ^8.0",
18 | "ext-json": "*",
19 | "doctrine/annotations": "^1.13 || ^2.0.1",
20 | "psr/log": "^1|^2|^3"
21 | },
22 | "require-dev": {
23 | "doctrine/collections": "^1.6",
24 | "friendsofphp/php-cs-fixer": "v3.17.0",
25 | "jms/serializer": "^2.3 || ^3",
26 | "phpstan/phpstan": "^1.0",
27 | "phpstan/phpstan-phpunit": "^1.0",
28 | "phpunit/phpunit": "^8.5.15 || ^9.5"
29 | },
30 | "suggest": {
31 | "jms/serializer": "^2.3 || ^3"
32 | },
33 | "conflict": {
34 | "doctrine/annotations": "< 1.11",
35 | "jms/serializer": "< 2.3"
36 | },
37 | "autoload": {
38 | "psr-4": {
39 | "Liip\\MetadataParser\\": "src/"
40 | }
41 | },
42 | "autoload-dev": {
43 | "psr-4": {
44 | "Tests\\Liip\\MetadataParser\\": "tests/"
45 | }
46 | },
47 | "scripts": {
48 | "fix-cs": "vendor/bin/php-cs-fixer fix -v",
49 | "cs-fixer": "vendor/bin/php-cs-fixer fix --dry-run --diff -v",
50 | "phpstan": "vendor/bin/phpstan analyse",
51 | "phpstan-tests": "vendor/bin/phpstan analyse -c phpstan.tests.neon",
52 | "phpstan-all": [
53 | "@phpstan",
54 | "@phpstan-tests"
55 | ],
56 | "phpunit": "vendor/bin/phpunit",
57 | "ci": [
58 | "@cs-fixer",
59 | "@phpstan-all",
60 | "@phpunit"
61 | ]
62 | },
63 | "config": {
64 | "sort-packages": true
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 5
3 | dynamicConstantNames:
4 | - PHP_VERSION_ID
5 | paths:
6 | - src/
7 | excludePaths:
8 | - src/ModelParser/JMSParserLegacy.php
9 |
--------------------------------------------------------------------------------
/phpstan.tests.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 1
3 | paths:
4 | - tests/
5 | excludePaths:
6 | - tests/ModelParser/JMSParserTestLegacy.php
7 | # looks like phpstan php parser has not implemented union types yet
8 | - tests/ModelParser/Fixtures/IntersectionTypeDeclarationModel.php
9 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | tests
10 |
11 |
12 |
13 |
14 |
15 | src
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/Annotation/Preferred.php:
--------------------------------------------------------------------------------
1 | metadata. It should then not
19 | * set the metadata on the property types. This would allow to work with
20 | * general model graphs that may include recursion.
21 | */
22 | final class Builder
23 | {
24 | /**
25 | * @var Parser
26 | */
27 | private $parser;
28 |
29 | /**
30 | * @var RecursionChecker
31 | */
32 | private $recursionChecker;
33 |
34 | public function __construct(Parser $parser, RecursionChecker $recursionChecker)
35 | {
36 | $this->parser = $parser;
37 | $this->recursionChecker = $recursionChecker;
38 | }
39 |
40 | /**
41 | * Build the tree for the specified class.
42 | *
43 | * This tree is guaranteed to be a tree and can not be a graph with recursion.
44 | *
45 | * @param PropertyReducerInterface[] $reducers
46 | *
47 | * @throws ParseException
48 | */
49 | public function build(string $className, array $reducers = []): ClassMetadata
50 | {
51 | $rawClassMetadataList = $this->parser->parse($className);
52 |
53 | /** @var ClassMetadata[] $classMetadataList */
54 | $classMetadataList = [];
55 | foreach ($rawClassMetadataList as $rawClassMetadata) {
56 | $classMetadataList[$rawClassMetadata->getClassName()] = PropertyReducer::reduce($rawClassMetadata, $reducers);
57 | }
58 |
59 | foreach ($classMetadataList as $classMetadata) {
60 | foreach ($classMetadata->getProperties() as $property) {
61 | try {
62 | $this->setTypeClassMetadata($property->getType(), $classMetadataList);
63 | } catch (\UnexpectedValueException $e) {
64 | throw ParseException::classNotParsed($e->getMessage(), (string) $classMetadata, (string) $property);
65 | }
66 | }
67 | }
68 |
69 | return $this->recursionChecker->check($classMetadataList[$className]);
70 | }
71 |
72 | /**
73 | * @param ClassMetadata[] $classMetadataList
74 | *
75 | * @throws \UnexpectedValueException if the class is not found
76 | */
77 | private function setTypeClassMetadata(PropertyType $type, array $classMetadataList): void
78 | {
79 | if ($type instanceof PropertyTypeClass) {
80 | if (!\array_key_exists($type->getClassName(), $classMetadataList)) {
81 | throw new \UnexpectedValueException($type->getClassName());
82 | }
83 | $type->setClassMetadata($classMetadataList[$type->getClassName()]);
84 |
85 | return;
86 | }
87 |
88 | if ($type instanceof PropertyTypeIterable) {
89 | $this->setTypeClassMetadata($type->getLeafType(), $classMetadataList);
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Exception/InvalidTypeException.php:
--------------------------------------------------------------------------------
1 | getCode() : 0,
16 | $previousException
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Exception/ParseException.php:
--------------------------------------------------------------------------------
1 | getCode(),
27 | $previousException
28 | );
29 | }
30 |
31 | public static function classError(string $className, \Exception $previousException): self
32 | {
33 | return new self(
34 | sprintf(self::CLASS_ERROR, $className, $previousException->getMessage()),
35 | $previousException->getCode(),
36 | $previousException
37 | );
38 | }
39 |
40 | public static function propertyError(string $className, string $propertyName, \Exception $previousException): self
41 | {
42 | return new self(
43 | sprintf(self::PROPERTY_ERROR, $className, $propertyName, $previousException->getMessage()),
44 | $previousException->getCode(),
45 | $previousException
46 | );
47 | }
48 |
49 | public static function propertyTypeError(string $className, string $propertyName, \Exception $previousException): self
50 | {
51 | return new self(
52 | sprintf(self::PROPERTY_TYPE_ERROR, $className, $propertyName, $previousException->getMessage()),
53 | $previousException->getCode(),
54 | $previousException
55 | );
56 | }
57 |
58 | public static function propertyTypeNameNull(string $className, string $propertyName): self
59 | {
60 | return new self(sprintf(self::PROPERTY_TYPE_NAME_NULL, $className, $propertyName));
61 | }
62 |
63 | public static function propertyTypeConflict(string $className, string $propertyName, string $typeA, string $typeB, \Exception $previousException): self
64 | {
65 | return new self(
66 | sprintf(self::PROPERTY_TYPE_CONFLICT, $className, $propertyName, $typeA, $typeB),
67 | $previousException->getCode(),
68 | $previousException
69 | );
70 | }
71 |
72 | public static function unsupportedClassAnnotation(string $className, string $annotation): self
73 | {
74 | return new self(sprintf(self::UNSUPPORTED_CLASS_ANNOTATION, $className, $annotation));
75 | }
76 |
77 | public static function unsupportedPropertyAnnotation(string $className, string $propertyName, string $annotation): self
78 | {
79 | return new self(sprintf(self::UNSUPPORTED_PROPERTY_ANNOTATION, $className, $propertyName, $annotation));
80 | }
81 |
82 | public static function nonPublicMethod(string $className, string $methodName): self
83 | {
84 | return new self(sprintf(self::NON_PUBLIC_METHOD, $className, $methodName));
85 | }
86 |
87 | public static function propertyAlreadyExists(string $propertyName, string $className): self
88 | {
89 | return new self(sprintf(self::PROPERTY_ALREADY_EXISTS, $propertyName, $className));
90 | }
91 |
92 | public static function classNotParsed(string $notFoundClassName, string $className, string $propertyName): self
93 | {
94 | return new self(sprintf(self::CLASS_NOT_PARSED, $notFoundClassName, $className, $propertyName));
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Exception/RecursionException.php:
--------------------------------------------------------------------------------
1 | name = $name;
60 | $this->setReadOnly($readOnly);
61 | $this->setPublic($public);
62 | $this->accessor = PropertyAccessor::none();
63 | $this->versionRange = VersionRange::all();
64 | }
65 |
66 | public function __toString(): string
67 | {
68 | return $this->name;
69 | }
70 |
71 | public function getName(): string
72 | {
73 | return $this->name;
74 | }
75 |
76 | public function isReadOnly(): bool
77 | {
78 | return $this->readOnly;
79 | }
80 |
81 | public function isPublic(): bool
82 | {
83 | return $this->public;
84 | }
85 |
86 | /**
87 | * @return string[]
88 | */
89 | public function getGroups(): array
90 | {
91 | return $this->groups;
92 | }
93 |
94 | public function getAccessor(): PropertyAccessor
95 | {
96 | return $this->accessor;
97 | }
98 |
99 | public function getVersionRange(): VersionRange
100 | {
101 | return $this->versionRange;
102 | }
103 |
104 | public function hasCustomInformation(string $key): bool
105 | {
106 | return \array_key_exists($key, $this->customInformation);
107 | }
108 |
109 | /**
110 | * Get the value stored as custom information, if it exists.
111 | *
112 | * @return mixed The information in whatever format it has been set
113 | *
114 | * @throws \InvalidArgumentException if no such custom value is available for this property
115 | */
116 | public function getCustomInformation(string $key): mixed
117 | {
118 | if (!\array_key_exists($key, $this->customInformation)) {
119 | throw new \InvalidArgumentException(sprintf('Property %s has no custom information %s', $this->name, $key));
120 | }
121 |
122 | return $this->customInformation[$key];
123 | }
124 |
125 | /**
126 | * Hashmap of custom information with the key as index and the information as value.
127 | *
128 | * @return mixed[]
129 | */
130 | public function getAllCustomInformation(): array
131 | {
132 | return $this->customInformation;
133 | }
134 |
135 | public function jsonSerialize(): array
136 | {
137 | $data = [
138 | 'name' => $this->name,
139 | 'is_public' => $this->public,
140 | 'is_read_only' => $this->readOnly,
141 | 'max_depth' => $this->maxDepth,
142 | ];
143 |
144 | if (\count($this->groups) > 0) {
145 | $data['groups'] = $this->groups;
146 | }
147 | if ($this->accessor->isDefined()) {
148 | $data['accessor'] = $this->accessor;
149 | }
150 | if ($this->versionRange->isDefined()) {
151 | $data['version'] = $this->versionRange;
152 | }
153 | if (\count($this->customInformation)) {
154 | $data['custom_information'] = array_map(
155 | static function ($information) {
156 | if (!\is_object($information) || $information instanceof \JsonSerializable) {
157 | return $information;
158 | }
159 |
160 | return '[non JsonSerializable custom information]';
161 | },
162 | $this->customInformation
163 | );
164 | }
165 |
166 | return $data;
167 | }
168 |
169 | abstract public function getType(): PropertyType;
170 |
171 | protected function setVersionRange(VersionRange $version): void
172 | {
173 | $this->versionRange = $version;
174 | }
175 |
176 | protected function setAccessor(PropertyAccessor $accessor): void
177 | {
178 | $this->accessor = $accessor;
179 | }
180 |
181 | /**
182 | * @param string[] $groups
183 | */
184 | protected function setGroups(array $groups): void
185 | {
186 | $this->groups = $groups;
187 | }
188 |
189 | protected function setReadOnly(bool $readOnly): void
190 | {
191 | $this->readOnly = $readOnly;
192 | }
193 |
194 | protected function setPublic(bool $public): void
195 | {
196 | $this->public = $public;
197 | }
198 |
199 | protected function setCustomInformation(string $key, $value): void
200 | {
201 | $this->customInformation[$key] = $value;
202 | }
203 |
204 | protected function getMaxDepth(): ?int
205 | {
206 | return $this->maxDepth;
207 | }
208 |
209 | protected function setMaxDepth(?int $maxDepth): void
210 | {
211 | $this->maxDepth = $maxDepth;
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/src/Metadata/AbstractPropertyType.php:
--------------------------------------------------------------------------------
1 | nullable = $nullable;
22 | }
23 |
24 | public function __toString(): string
25 | {
26 | return $this->isNullable() ? '|null' : '';
27 | }
28 |
29 | public function isNullable(): bool
30 | {
31 | return $this->nullable;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Metadata/ClassMetadata.php:
--------------------------------------------------------------------------------
1 | className = $className;
53 | $this->constructorParameters = $constructorParameters;
54 | $this->postDeserializeMethods = $postDeserializeMethods;
55 |
56 | foreach ($properties as $property) {
57 | $this->addProperty($property);
58 | }
59 | }
60 |
61 | public function __toString(): string
62 | {
63 | return $this->className;
64 | }
65 |
66 | /**
67 | * @param PropertyMetadata[] $properties
68 | */
69 | public static function fromRawClassMetadata(RawClassMetadata $rawClassMetadata, array $properties): self
70 | {
71 | return new self(
72 | $rawClassMetadata->getClassName(),
73 | $properties,
74 | $rawClassMetadata->getConstructorParameters(),
75 | $rawClassMetadata->getPostDeserializeMethods()
76 | );
77 | }
78 |
79 | public function getClassName(): string
80 | {
81 | return $this->className;
82 | }
83 |
84 | /**
85 | * @return PropertyMetadata[]
86 | */
87 | public function getProperties(): array
88 | {
89 | return $this->properties;
90 | }
91 |
92 | /**
93 | * @return string[]
94 | */
95 | public function getPostDeserializeMethods(): array
96 | {
97 | return $this->postDeserializeMethods;
98 | }
99 |
100 | /**
101 | * @return ParameterMetadata[]
102 | */
103 | public function getConstructorParameters(): array
104 | {
105 | return $this->constructorParameters;
106 | }
107 |
108 | public function hasConstructorParameter(string $name): bool
109 | {
110 | foreach ($this->constructorParameters as $parameter) {
111 | if ($parameter->getName() === $name) {
112 | return true;
113 | }
114 | }
115 |
116 | return false;
117 | }
118 |
119 | public function getConstructorParameter(string $name): ParameterMetadata
120 | {
121 | foreach ($this->constructorParameters as $parameter) {
122 | if ($parameter->getName() === $name) {
123 | return $parameter;
124 | }
125 | }
126 |
127 | throw new \InvalidArgumentException(sprintf('Class %s has no constructor parameter called "%s"', $this->className, $name));
128 | }
129 |
130 | /**
131 | * Returns a copy of the class metadata with the specified properties removed.
132 | *
133 | * This can be used for expected recursions to remove the affected properties.
134 | *
135 | * @param string[] $propertyNames
136 | */
137 | public function withoutProperties(array $propertyNames): self
138 | {
139 | $properties = array_values(array_filter(
140 | $this->properties,
141 | static function (PropertyMetadata $property) use ($propertyNames): bool {
142 | return !\in_array($property->getName(), $propertyNames, true);
143 | }
144 | ));
145 |
146 | return new self(
147 | $this->className,
148 | $properties,
149 | $this->constructorParameters,
150 | $this->postDeserializeMethods
151 | );
152 | }
153 |
154 | public function jsonSerialize(): array
155 | {
156 | return array_filter([
157 | 'class_name' => $this->className,
158 | 'properties' => $this->properties,
159 | 'post_deserialize_method' => $this->postDeserializeMethods,
160 | 'constructor_parameters' => $this->constructorParameters,
161 | ]);
162 | }
163 |
164 | /**
165 | * @throws ParseException if the property already exists
166 | */
167 | private function addProperty(PropertyMetadata $property): void
168 | {
169 | foreach ($this->properties as $prop) {
170 | if ($prop->getSerializedName() === $property->getSerializedName()) {
171 | throw ParseException::propertyAlreadyExists((string) $property, (string) $this);
172 | }
173 | }
174 |
175 | $this->properties[] = $property;
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/Metadata/DateTimeOptions.php:
--------------------------------------------------------------------------------
1 | format = $format;
37 | $this->zone = $zone;
38 | $this->deserializeFormats = \is_string($deserializeFormats) ? [$deserializeFormats] : $deserializeFormats;
39 | }
40 |
41 | public function getFormat(): ?string
42 | {
43 | return $this->format;
44 | }
45 |
46 | public function getZone(): ?string
47 | {
48 | return $this->zone;
49 | }
50 |
51 | /**
52 | * @deprecated Please use {@see getDeserializeFormats}
53 | */
54 | public function getDeserializeFormat(): ?string
55 | {
56 | foreach ($this->deserializeFormats ?? [] as $format) {
57 | return $format;
58 | }
59 |
60 | return null;
61 | }
62 |
63 | public function getDeserializeFormats(): ?array
64 | {
65 | return $this->deserializeFormats;
66 | }
67 |
68 | public function jsonSerialize(): array
69 | {
70 | return array_filter([
71 | 'format' => $this->format,
72 | 'zone' => $this->zone,
73 | 'deserialize_format' => $this->getDeserializeFormat(),
74 | 'deserialize_formats' => $this->deserializeFormats,
75 | ]);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Metadata/ParameterMetadata.php:
--------------------------------------------------------------------------------
1 | name = $name;
27 | $this->required = $required;
28 | $this->defaultValue = $defaultValue;
29 | }
30 |
31 | public function __toString(): string
32 | {
33 | return $this->name;
34 | }
35 |
36 | public static function fromReflection(\ReflectionParameter $reflParameter): self
37 | {
38 | if ($reflParameter->isOptional()) {
39 | return new self($reflParameter->getName(), false, $reflParameter->getDefaultValue());
40 | }
41 |
42 | return new self($reflParameter->getName(), true);
43 | }
44 |
45 | public function getName(): string
46 | {
47 | return $this->name;
48 | }
49 |
50 | public function isRequired(): bool
51 | {
52 | return $this->required;
53 | }
54 |
55 | /**
56 | * @throws \BadMethodCallException if the parameter is required and therefore has no default value
57 | */
58 | public function getDefaultValue()
59 | {
60 | if ($this->required) {
61 | throw new \BadMethodCallException(sprintf('Parameter %s is required and therefore has no default value', (string) $this));
62 | }
63 |
64 | return $this->defaultValue;
65 | }
66 |
67 | public function jsonSerialize(): array
68 | {
69 | return [
70 | 'name' => $this->name,
71 | 'required' => $this->required,
72 | 'default_value' => $this->defaultValue,
73 | ];
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Metadata/PropertyAccessor.php:
--------------------------------------------------------------------------------
1 | getterMethod = $getterMethod;
16 | $this->setterMethod = $setterMethod;
17 | }
18 |
19 | public static function none(): self
20 | {
21 | return new self(null, null);
22 | }
23 |
24 | public function isDefined(): bool
25 | {
26 | return $this->hasGetterMethod() || $this->hasSetterMethod();
27 | }
28 |
29 | public function hasGetterMethod(): bool
30 | {
31 | return null !== $this->getterMethod;
32 | }
33 |
34 | public function getGetterMethod(): ?string
35 | {
36 | return $this->getterMethod;
37 | }
38 |
39 | public function hasSetterMethod(): bool
40 | {
41 | return null !== $this->setterMethod;
42 | }
43 |
44 | public function getSetterMethod(): ?string
45 | {
46 | return $this->setterMethod;
47 | }
48 |
49 | public function jsonSerialize(): array
50 | {
51 | return array_filter([
52 | 'getter_method' => $this->getterMethod,
53 | 'setter_method' => $this->setterMethod,
54 | ]);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Metadata/PropertyMetadata.php:
--------------------------------------------------------------------------------
1 | serializedName = $serializedName;
39 | $this->type = $type ?: new PropertyTypeUnknown(true);
40 | $this->setVersionRange($versionRange ?: new VersionRange(null, null));
41 | $this->setGroups($groups);
42 | if ($accessor) {
43 | $this->setAccessor($accessor);
44 | }
45 | foreach ($customInformation as $key => $value) {
46 | $this->setCustomInformation((string) $key, $value);
47 | }
48 |
49 | $this->setMaxDepth($maxDepth);
50 | }
51 |
52 | public function __toString(): string
53 | {
54 | return $this->serializedName;
55 | }
56 |
57 | public static function fromRawProperty(string $serializedName, PropertyVariationMetadata $property): self
58 | {
59 | return new self(
60 | $serializedName,
61 | $property->getName(),
62 | $property->getType(),
63 | $property->isReadOnly(),
64 | $property->isPublic(),
65 | $property->getVersionRange(),
66 | $property->getGroups(),
67 | $property->getAccessor(),
68 | $property->getAllCustomInformation(),
69 | $property->getMaxDepth()
70 | );
71 | }
72 |
73 | public function getType(): PropertyType
74 | {
75 | return $this->type;
76 | }
77 |
78 | public function getSerializedName(): string
79 | {
80 | return $this->serializedName;
81 | }
82 |
83 | public function getMaxDepth(): ?int
84 | {
85 | return parent::getMaxDepth();
86 | }
87 |
88 | public function jsonSerialize(): array
89 | {
90 | return array_merge(
91 | [
92 | 'serialized_name' => $this->serializedName,
93 | ],
94 | parent::jsonSerialize(),
95 | [
96 | 'type' => (string) $this->type,
97 | ]
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Metadata/PropertyType.php:
--------------------------------------------------------------------------------
1 | subType = $subType;
34 | $this->hashmap = $hashmap;
35 | $this->isCollection = $isCollection;
36 | }
37 |
38 | public function __toString(): string
39 | {
40 | if ($this->subType instanceof PropertyTypeUnknown) {
41 | return 'array'.($this->isCollection ? '|\\'.Collection::class : '');
42 | }
43 |
44 | $array = $this->isHashmap() ? '[string]' : '[]';
45 | if ($this->isCollection) {
46 | $collectionType = $this->isHashmap() ? ', string' : '';
47 | $array .= sprintf('|\\%s<%s%s>', Collection::class, $this->subType, $collectionType);
48 | }
49 |
50 | return ((string) $this->subType).$array.parent::__toString();
51 | }
52 |
53 | public function isHashmap(): bool
54 | {
55 | return $this->hashmap;
56 | }
57 |
58 | /**
59 | * Returns the type of the next level, which could be an array or hashmap or another type.
60 | */
61 | public function getSubType(): PropertyType
62 | {
63 | return $this->subType;
64 | }
65 |
66 | /**
67 | * @deprecated Please prefer using {@link isTraversable}
68 | */
69 | public function isCollection(): bool
70 | {
71 | return $this->isCollection;
72 | }
73 |
74 | /**
75 | * @deprecated Please prefer using {@link getTraversableClass}
76 | *
77 | * @return class-string|null
78 | */
79 | public function getCollectionClass(): ?string
80 | {
81 | return $this->isCollection() ? Collection::class : null;
82 | }
83 |
84 | public function isTraversable(): bool
85 | {
86 | return $this->isCollection;
87 | }
88 |
89 | /**
90 | * @return class-string<\Traversable>
91 | */
92 | public function getTraversableClass(): string
93 | {
94 | if (!$this->isTraversable()) {
95 | throw new \UnexpectedValueException("Iterable type '{$this}' is not traversable.");
96 | }
97 |
98 | return \Traversable::class;
99 | }
100 |
101 | /**
102 | * Goes down the type until it is not an array or hashmap anymore.
103 | */
104 | public function getLeafType(): PropertyType
105 | {
106 | $type = $this->getSubType();
107 | while ($type instanceof self) {
108 | $type = $type->getSubType();
109 | }
110 |
111 | return $type;
112 | }
113 |
114 | public function merge(PropertyType $other): PropertyType
115 | {
116 | $nullable = $this->isNullable() && $other->isNullable();
117 |
118 | if ($other instanceof PropertyTypeUnknown) {
119 | return new self($this->subType, $this->isHashmap(), $nullable);
120 | }
121 | if ($this->isCollection() && (($other instanceof PropertyTypeClass) && is_a($other->getClassName(), Collection::class, true))) {
122 | return new self($this->getSubType(), $this->isHashmap(), $nullable, true);
123 | }
124 | if (!$other instanceof self) {
125 | throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, they must be the same or unknown', self::class, \get_class($other)));
126 | }
127 |
128 | /*
129 | * We allow converting array to hashmap (but not the other way round).
130 | *
131 | * PHPDoc has no clear definition for hashmaps with string indexes, but JMS Serializer annotations do.
132 | */
133 | if ($this->isHashmap() && !$other->isHashmap()) {
134 | throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, can\'t change hashmap into plain array', self::class, \get_class($other)));
135 | }
136 |
137 | $hashmap = $this->isHashmap() || $other->isHashmap();
138 | $isCollection = $this->isCollection || $other->isCollection;
139 | if ($other->getSubType() instanceof PropertyTypeUnknown) {
140 | return new self($this->getSubType(), $hashmap, $nullable, $isCollection);
141 | }
142 | if ($this->getSubType() instanceof PropertyTypeUnknown) {
143 | return new self($other->getSubType(), $hashmap, $nullable, $isCollection);
144 | }
145 |
146 | return new self($this->getSubType()->merge($other->getSubType()), $hashmap, $nullable, $isCollection);
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/Metadata/PropertyTypeClass.php:
--------------------------------------------------------------------------------
1 | className = $className;
33 | }
34 |
35 | public function __toString(): string
36 | {
37 | return $this->className.parent::__toString();
38 | }
39 |
40 | public function getClassName(): string
41 | {
42 | return $this->className;
43 | }
44 |
45 | public function getClassMetadata(): ClassMetadata
46 | {
47 | if (null === $this->classMetadata) {
48 | throw new \BadMethodCallException('Internal error, custom class property type is missing the metadata. Looks like the schema builder didn\'t set it, which is a bug');
49 | }
50 |
51 | return $this->classMetadata;
52 | }
53 |
54 | /**
55 | * This method is only to be used by the parsing process and is required to avoid a chicken-and-egg problem.
56 | *
57 | * @internal
58 | */
59 | public function setClassMetadata(ClassMetadata $classMetadata): void
60 | {
61 | $this->classMetadata = $classMetadata;
62 | }
63 |
64 | public function merge(PropertyType $other): PropertyType
65 | {
66 | $nullable = $this->isNullable() && $other->isNullable();
67 |
68 | if ($other instanceof PropertyTypeUnknown) {
69 | return new self($this->className, $nullable);
70 | }
71 | if (is_a($this->getClassName(), Collection::class, true) && (($other instanceof PropertyTypeIterable) && $other->isTraversable())) {
72 | return $other->merge($this);
73 | }
74 | if (is_a($this->getClassName(), \DateTimeInterface::class, true) && ($other instanceof PropertyTypeDateTime)) {
75 | return $other->merge($this);
76 | }
77 | if (!$other instanceof self) {
78 | throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, they must be the same or unknown', self::class, \get_class($other)));
79 | }
80 | if ($this->getClassName() !== $other->getClassName()) {
81 | throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, they must be equal', self::class, \get_class($other)));
82 | }
83 |
84 | return new self($this->className, $nullable);
85 | }
86 |
87 | public static function isTypeCustomClass(string $typeName): bool
88 | {
89 | return !PropertyTypePrimitive::isTypePrimitive($typeName)
90 | && !PropertyTypeDateTime::isTypeDateTime($typeName);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Metadata/PropertyTypeDateTime.php:
--------------------------------------------------------------------------------
1 | immutable = $immutable;
28 | $this->dateTimeOptions = $dateTimeOptions;
29 | }
30 |
31 | public function __toString(): string
32 | {
33 | $class = $this->immutable ? \DateTimeImmutable::class : \DateTime::class;
34 |
35 | return $class.parent::__toString();
36 | }
37 |
38 | public function isImmutable(): bool
39 | {
40 | return $this->immutable;
41 | }
42 |
43 | public function getFormat(): ?string
44 | {
45 | if ($this->dateTimeOptions) {
46 | return $this->dateTimeOptions->getFormat();
47 | }
48 |
49 | return null;
50 | }
51 |
52 | public function getZone(): ?string
53 | {
54 | if ($this->dateTimeOptions) {
55 | return $this->dateTimeOptions->getZone();
56 | }
57 |
58 | return null;
59 | }
60 |
61 | /**
62 | * @deprecated Please prefer {@link getDeserializeFormats}
63 | */
64 | public function getDeserializeFormat(): ?string
65 | {
66 | if ($this->dateTimeOptions) {
67 | return $this->dateTimeOptions->getDeserializeFormat();
68 | }
69 |
70 | return null;
71 | }
72 |
73 | public function getDeserializeFormats(): ?array
74 | {
75 | if ($this->dateTimeOptions) {
76 | return $this->dateTimeOptions->getDeserializeFormats();
77 | }
78 |
79 | return null;
80 | }
81 |
82 | public function merge(PropertyType $other): PropertyType
83 | {
84 | $nullable = $this->isNullable() && $other->isNullable();
85 |
86 | if ($other instanceof PropertyTypeUnknown) {
87 | return new self($this->immutable, $nullable, $this->dateTimeOptions);
88 | }
89 | if (($other instanceof PropertyTypeClass) && is_a($other->getClassName(), \DateTimeInterface::class, true)) {
90 | if (is_a($other->getClassName(), \DateTimeImmutable::class, true)) {
91 | return new self(true, $nullable, $this->dateTimeOptions);
92 | }
93 | if (is_a($other->getClassName(), \DateTime::class, true) || (\DateTimeInterface::class === $other->getClassName())) {
94 | return new self(false, $nullable, $this->dateTimeOptions);
95 | }
96 |
97 | throw new \UnexpectedValueException("Can't merge type '{$this}' with '{$other}', they must be the same or unknown");
98 | }
99 | if (!$other instanceof self) {
100 | throw new \UnexpectedValueException("Can't merge type '{$this}' with '{$other}', they must be the same or unknown");
101 | }
102 | if ($this->isImmutable() !== $other->isImmutable()) {
103 | throw new \UnexpectedValueException("Can't merge type '{$this}' with '{$other}', they must be equal");
104 | }
105 |
106 | $options = $this->dateTimeOptions ?: $other->dateTimeOptions;
107 |
108 | return new self($this->immutable, $nullable, $options);
109 | }
110 |
111 | public static function fromDateTimeClass(string $className, bool $nullable, DateTimeOptions $dateTimeOptions = null): self
112 | {
113 | if (!self::isTypeDateTime($className)) {
114 | throw new \UnexpectedValueException(sprintf('Given type "%s" is not date time class or interface', $className));
115 | }
116 |
117 | return new self(\DateTimeImmutable::class === $className, $nullable, $dateTimeOptions);
118 | }
119 |
120 | public static function isTypeDateTime(string $typeName): bool
121 | {
122 | return \in_array($typeName, self::DATE_TIME_TYPES, true);
123 | }
124 |
125 | /**
126 | * Find the most derived class that doesn't deny both class hints, meaning the most derived
127 | * between left and right if one is a child of the other
128 | */
129 | protected function findCommonDateTimeClass(?string $left, ?string $right): ?string
130 | {
131 | if (null === $right) {
132 | return $left;
133 | }
134 | if (null === $left) {
135 | return $right;
136 | }
137 |
138 | if (is_a($left, $right, true)) {
139 | return $left;
140 | }
141 | if (is_a($right, $left, true)) {
142 | return $right;
143 | }
144 |
145 | throw new \UnexpectedValueException("Collection classes '{$left}' and '{$right}' do not match.");
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/Metadata/PropertyTypeIterable.php:
--------------------------------------------------------------------------------
1 | , provided that T is, inherits from, or is a parent class of $this->collectionClass
11 | * This property type can be merged with PropertyTypeIterable, if :
12 | * - we're not merging a plain array PropertyTypeIterable into a hashmap one,
13 | * - and the collection classes of each are either not present on both sides, or are the same, or parent-child of one another
14 | */
15 | final class PropertyTypeIterable extends PropertyTypeArray
16 | {
17 | /**
18 | * @var string
19 | */
20 | private $traversableClass;
21 |
22 | /**
23 | * @param class-string<\Traversable>|null $traversableClass
24 | */
25 | public function __construct(PropertyType $subType, bool $hashmap, bool $nullable, string $traversableClass = null)
26 | {
27 | parent::__construct($subType, $hashmap, $nullable, null != $traversableClass);
28 |
29 | $this->traversableClass = $traversableClass;
30 | }
31 |
32 | public function __toString(): string
33 | {
34 | if ($this->subType instanceof PropertyTypeUnknown) {
35 | return 'array'.($this->isTraversable() ? '|\\'.$this->traversableClass : '');
36 | }
37 |
38 | $array = $this->isHashmap() ? '[string]' : '[]';
39 | if ($this->isTraversable()) {
40 | $collectionType = $this->isHashmap() ? ', string' : '';
41 | $array .= sprintf('|\\%s<%s%s>', $this->traversableClass, $this->subType, $collectionType);
42 | }
43 |
44 | return ((string) $this->subType).$array.AbstractPropertyType::__toString();
45 | }
46 |
47 | /**
48 | * @deprecated Please prefer using {@link getTraversableClass}
49 | *
50 | * @return class-string|null
51 | */
52 | public function getCollectionClass(): ?string
53 | {
54 | return $this->isCollection() ? null : $this->traversableClass;
55 | }
56 |
57 | /**
58 | * @deprecated Please prefer using {@link isTraversable}
59 | */
60 | public function isCollection(): bool
61 | {
62 | return (null != $this->traversableClass) && is_a($this->traversableClass, Collection::class, true);
63 | }
64 |
65 | /**
66 | * @return class-string<\Traversable>
67 | */
68 | public function getTraversableClass(): string
69 | {
70 | if (!$this->isTraversable()) {
71 | throw new \UnexpectedValueException("Iterable type '{$this}' is not traversable.");
72 | }
73 |
74 | return $this->traversableClass;
75 | }
76 |
77 | public function isTraversable(): bool
78 | {
79 | return null != $this->traversableClass;
80 | }
81 |
82 | public function merge(PropertyType $other): PropertyType
83 | {
84 | $nullable = $this->isNullable() && $other->isNullable();
85 | $thisTraversableClass = $this->isTraversable() ? $this->getTraversableClass() : null;
86 |
87 | if ($other instanceof PropertyTypeUnknown) {
88 | return new self($this->subType, $this->isHashmap(), $nullable, $thisTraversableClass);
89 | }
90 | if ($this->isTraversable() && (($other instanceof PropertyTypeClass) && is_a($other->getClassName(), \Traversable::class, true))) {
91 | return new self($this->getSubType(), $this->isHashmap(), $nullable, $this->findCommonCollectionClass($thisTraversableClass, $other->getClassName()));
92 | }
93 | if (!$other instanceof parent) {
94 | throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, they must be the same or unknown', self::class, \get_class($other)));
95 | }
96 |
97 | /*
98 | * We allow converting array to hashmap (but not the other way round).
99 | *
100 | * PHPDoc has no clear definition for hashmaps with string indexes, but JMS Serializer annotations do.
101 | */
102 | if ($this->isHashmap() && !$other->isHashmap()) {
103 | throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, can\'t change hashmap into plain array', self::class, \get_class($other)));
104 | }
105 |
106 | $otherTraversableClass = $other->isTraversable() ? $other->getTraversableClass() : null;
107 | $hashmap = $this->isHashmap() || $other->isHashmap();
108 | $commonClass = $this->findCommonCollectionClass($thisTraversableClass, $otherTraversableClass);
109 |
110 | if ($other->getSubType() instanceof PropertyTypeUnknown) {
111 | return new self($this->getSubType(), $hashmap, $nullable, $commonClass);
112 | }
113 | if ($this->getSubType() instanceof PropertyTypeUnknown) {
114 | return new self($other->getSubType(), $hashmap, $nullable, $commonClass);
115 | }
116 |
117 | return new self($this->getSubType()->merge($other->getSubType()), $hashmap, $nullable, $commonClass);
118 | }
119 |
120 | /**
121 | * Find the most derived class that doesn't deny both class hints, meaning the most derived
122 | * between left and right if one is a child of the other
123 | */
124 | private function findCommonCollectionClass(?string $left, ?string $right): ?string
125 | {
126 | if (null === $right) {
127 | return $left;
128 | }
129 | if (null === $left) {
130 | return $right;
131 | }
132 |
133 | if (is_a($left, $right, true)) {
134 | return $left;
135 | }
136 | if (is_a($right, $left, true)) {
137 | return $right;
138 | }
139 |
140 | throw new \UnexpectedValueException("Traversable classes '{$left}' and '{$right}' do not match.");
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/Metadata/PropertyTypePrimitive.php:
--------------------------------------------------------------------------------
1 | 'bool',
11 | 'integer' => 'int',
12 | 'double' => 'float',
13 | 'real' => 'float',
14 | ];
15 |
16 | private const PRIMITIVE_TYPES = [
17 | 'string',
18 | 'int',
19 | 'float',
20 | 'bool',
21 | ];
22 |
23 | /**
24 | * @var string|null
25 | */
26 | private $typeName;
27 |
28 | public function __construct(string $typeName, bool $nullable)
29 | {
30 | parent::__construct($nullable);
31 | if (\array_key_exists($typeName, self::TYPE_MAP)) {
32 | $typeName = self::TYPE_MAP[$typeName];
33 | }
34 | if (!self::isTypePrimitive($typeName)) {
35 | throw new \UnexpectedValueException(sprintf('Given type "%s" is not primitive', $typeName));
36 | }
37 | $this->typeName = $typeName;
38 | }
39 |
40 | public function __toString(): string
41 | {
42 | return $this->typeName.parent::__toString();
43 | }
44 |
45 | public function getTypeName(): string
46 | {
47 | return $this->typeName;
48 | }
49 |
50 | public function merge(PropertyType $other): PropertyType
51 | {
52 | $nullable = $this->isNullable() && $other->isNullable();
53 |
54 | if ($other instanceof PropertyTypeUnknown) {
55 | return new self($this->typeName, $nullable);
56 | }
57 | if (!$other instanceof self) {
58 | throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, they must be the same or unknown', self::class, \get_class($other)));
59 | }
60 | if ($this->getTypeName() !== $other->getTypeName()) {
61 | throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, they must be equal', self::class, \get_class($other)));
62 | }
63 |
64 | return new self($this->typeName, $nullable);
65 | }
66 |
67 | public static function isTypePrimitive(string $typeName): bool
68 | {
69 | if (\array_key_exists($typeName, self::TYPE_MAP)) {
70 | $typeName = self::TYPE_MAP[$typeName];
71 | }
72 |
73 | return \in_array($typeName, self::PRIMITIVE_TYPES, true);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Metadata/PropertyTypeUnknown.php:
--------------------------------------------------------------------------------
1 | isNullable() && $other->isNullable());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Metadata/VersionRange.php:
--------------------------------------------------------------------------------
1 | since = $since;
25 | $this->until = $until;
26 | }
27 |
28 | public static function all(): self
29 | {
30 | return new self(null, null);
31 | }
32 |
33 | public function isDefined(): bool
34 | {
35 | return null !== $this->since || null !== $this->until;
36 | }
37 |
38 | /**
39 | * Check if this version range allows a lower version than the other version range.
40 | *
41 | * Returns false if both have the same lower bound.
42 | */
43 | public function allowsLowerThan(self $other): bool
44 | {
45 | if (null === $other->since) {
46 | return false;
47 | }
48 | if (null === $this->since) {
49 | return true;
50 | }
51 |
52 | return version_compare($this->since, $other->since, '<');
53 | }
54 |
55 | /**
56 | * Check if this version range allows a higher version than the other version range.
57 | *
58 | * Returns false if both have the same upper bound.
59 | */
60 | public function allowsHigherThan(self $other)
61 | {
62 | if (null === $this->until) {
63 | return false;
64 | }
65 | if (null === $other->until) {
66 | return true;
67 | }
68 |
69 | return version_compare($this->until, $other->until, '>');
70 | }
71 |
72 | /**
73 | * The lowest allowed version or `null` when there is no lower bound.
74 | */
75 | public function getSince(): ?string
76 | {
77 | return $this->since;
78 | }
79 |
80 | /**
81 | * The highest allowed version or `null` when there is no upper bound.
82 | */
83 | public function getUntil(): ?string
84 | {
85 | return $this->until;
86 | }
87 |
88 | public function withSince(string $since): self
89 | {
90 | $copy = clone $this;
91 | $copy->since = $since;
92 |
93 | return $copy;
94 | }
95 |
96 | public function withUntil(string $until): self
97 | {
98 | $copy = clone $this;
99 | $copy->until = $until;
100 |
101 | return $copy;
102 | }
103 |
104 | public function isIncluded(string $version): bool
105 | {
106 | if (null !== $this->since && version_compare($version, $this->since, '<')) {
107 | return false;
108 | }
109 | if (null !== $this->until && version_compare($version, $this->until, '>')) {
110 | return false;
111 | }
112 |
113 | return true;
114 | }
115 |
116 | public function jsonSerialize(): array
117 | {
118 | return array_filter([
119 | 'since' => $this->since,
120 | 'until' => $this->until,
121 | ]);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/ModelParser/JMSParserLegacy.php:
--------------------------------------------------------------------------------
1 | setReadOnly($annotation->readOnly);
24 |
25 | return true;
26 | }
27 |
28 | return false;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/ModelParser/LiipMetadataAnnotationParser.php:
--------------------------------------------------------------------------------
1 | annotationsReader = $annotationsReader;
32 | }
33 |
34 | public function parse(RawClassMetadata $classMetadata): void
35 | {
36 | try {
37 | $reflClass = new \ReflectionClass($classMetadata->getClassName());
38 | } catch (\ReflectionException $e) {
39 | throw ParseException::classNotFound($classMetadata->getClassName(), $e);
40 | }
41 |
42 | $this->parseProperties($reflClass, $classMetadata);
43 | $this->parseMethods($reflClass, $classMetadata);
44 | }
45 |
46 | private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
47 | {
48 | if ($reflParentClass = $reflClass->getParentClass()) {
49 | $this->parseProperties($reflParentClass, $classMetadata);
50 | }
51 |
52 | foreach ($reflClass->getProperties() as $reflProperty) {
53 | if (!$classMetadata->hasPropertyVariation($reflProperty->getName())) {
54 | continue;
55 | }
56 |
57 | try {
58 | $annotations = $this->annotationsReader->getPropertyAnnotations($reflProperty);
59 | } catch (AnnotationException $e) {
60 | throw ParseException::propertyError((string) $classMetadata, $reflProperty->getName(), $e);
61 | }
62 |
63 | $property = $classMetadata->getPropertyVariation($reflProperty->getName());
64 | $this->parsePropertyAnnotations($classMetadata, $property, $annotations);
65 | }
66 | }
67 |
68 | private function parseMethods(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
69 | {
70 | if ($reflParentClass = $reflClass->getParentClass()) {
71 | $this->parseMethods($reflParentClass, $classMetadata);
72 | }
73 |
74 | foreach ($reflClass->getMethods() as $reflMethod) {
75 | if (false === $reflMethod->getDocComment()) {
76 | continue;
77 | }
78 | if (!$classMetadata->hasPropertyVariation($this->getMethodName($reflMethod))) {
79 | continue;
80 | }
81 |
82 | try {
83 | $annotations = $this->annotationsReader->getMethodAnnotations($reflMethod);
84 | } catch (AnnotationException $e) {
85 | throw ParseException::propertyError((string) $classMetadata, $reflMethod->getName(), $e);
86 | }
87 |
88 | $property = $classMetadata->getPropertyVariation($this->getMethodName($reflMethod));
89 | $this->parsePropertyAnnotations($classMetadata, $property, $annotations);
90 | }
91 | }
92 |
93 | private function parsePropertyAnnotations(RawClassMetadata $classMetadata, PropertyVariationMetadata $property, array $annotations): void
94 | {
95 | foreach ($annotations as $annotation) {
96 | switch (true) {
97 | case $annotation instanceof Preferred:
98 | $property->setPreferred(true);
99 | break;
100 |
101 | default:
102 | if (0 === strncmp('Liip\MetadataParser\\', \get_class($annotation), mb_strlen('Liip\MetadataParser\\'))) {
103 | // if there are annotations we can safely ignore, we need to explicitly ignore them
104 | throw ParseException::unsupportedPropertyAnnotation((string) $classMetadata, (string) $property, \get_class($annotation));
105 | }
106 | break;
107 | }
108 | }
109 | }
110 |
111 | private function getMethodName(\ReflectionMethod $reflMethod): string
112 | {
113 | $name = $reflMethod->getName();
114 | if (0 === strpos($name, 'get')) {
115 | $name = lcfirst(substr($name, 3));
116 | }
117 |
118 | return $name;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/ModelParser/ModelParserInterface.php:
--------------------------------------------------------------------------------
1 | root = $root;
24 | }
25 |
26 | public function __toString(): string
27 | {
28 | if (0 === \count($this->stack)) {
29 | return $this->root;
30 | }
31 |
32 | $stack = array_map(static function (PropertyVariationMetadata $propertyMetadata) {
33 | return $propertyMetadata->getName();
34 | }, $this->stack);
35 |
36 | return sprintf('%s->%s', $this->root, implode('->', $stack));
37 | }
38 |
39 | public function push(PropertyVariationMetadata $property): self
40 | {
41 | $context = clone $this;
42 | $context->stack[] = $property;
43 |
44 | return $context;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/ModelParser/PhpDocParser.php:
--------------------------------------------------------------------------------
1 | typeParser = new PhpTypeParser();
25 | }
26 |
27 | public function parse(RawClassMetadata $classMetadata): void
28 | {
29 | try {
30 | $reflClass = new \ReflectionClass($classMetadata->getClassName());
31 | } catch (\ReflectionException $e) {
32 | throw ParseException::classNotFound($classMetadata->getClassName(), $e);
33 | }
34 |
35 | $this->parseProperties($reflClass, $classMetadata);
36 | }
37 |
38 | /**
39 | * @return string[] the property names that have been added
40 | */
41 | private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): array
42 | {
43 | $existingProperties = array_map(static function (PropertyCollection $prop): string {
44 | return (string) $prop;
45 | }, $classMetadata->getPropertyCollections());
46 |
47 | $addedProperties = [];
48 | $parentProperties = [];
49 | if ($reflParentClass = $reflClass->getParentClass()) {
50 | $parentProperties = $this->parseProperties($reflParentClass, $classMetadata);
51 | }
52 | $parentPropertiesLookup = array_flip($parentProperties);
53 |
54 | foreach ($reflClass->getProperties() as $reflProperty) {
55 | if ($classMetadata->hasPropertyVariation($reflProperty->getName())) {
56 | $property = $classMetadata->getPropertyVariation($reflProperty->getName());
57 | } else {
58 | $property = PropertyVariationMetadata::fromReflection($reflProperty);
59 | $classMetadata->addPropertyVariation($reflProperty->getName(), $property);
60 | }
61 |
62 | $docComment = $reflProperty->getDocComment();
63 | if (false !== $docComment) {
64 | try {
65 | $type = $this->getPropertyTypeFromDocComment($docComment, $reflProperty);
66 | } catch (ParseException $e) {
67 | throw ParseException::propertyTypeError((string) $classMetadata, (string) $property, $e);
68 | }
69 | if (null === $type) {
70 | continue;
71 | }
72 |
73 | if ($property->getType() instanceof PropertyTypeUnknown || \array_key_exists((string) $property, $parentPropertiesLookup)) {
74 | $property->setType($type);
75 | } else {
76 | try {
77 | $property->setType($property->getType()->merge($type));
78 | } catch (\UnexpectedValueException $e) {
79 | throw ParseException::propertyTypeConflict((string) $classMetadata, (string) $property, (string) $property->getType(), (string) $type, $e);
80 | }
81 | }
82 | $addedProperties[] = (string) $property;
83 | }
84 | }
85 |
86 | return array_values(array_diff(array_unique(array_merge($parentProperties, $addedProperties)), $existingProperties));
87 | }
88 |
89 | private function getPropertyTypeFromDocComment(string $docComment, \ReflectionProperty $reflProperty): ?PropertyType
90 | {
91 | foreach (explode("\n", $docComment) as $line) {
92 | if (1 === preg_match('/@var ([^ ]+)/', $line, $matches)) {
93 | return $this->typeParser->parseAnnotationType($matches[1], $reflProperty->getDeclaringClass());
94 | }
95 | }
96 |
97 | return null;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/ModelParser/RawMetadata/PropertyCollection.php:
--------------------------------------------------------------------------------
1 | setSerializedName($name);
24 | }
25 |
26 | public function __toString(): string
27 | {
28 | return $this->serializedName;
29 | }
30 |
31 | public static function serializedName(string $name): string
32 | {
33 | if (self::$identicalNamingStrategy) {
34 | return $name;
35 | }
36 |
37 | return strtolower(preg_replace('/[A-Z]/', '_\\0', $name));
38 | }
39 |
40 | public static function useIdenticalNamingStrategy($value = true): void
41 | {
42 | self::$identicalNamingStrategy = $value;
43 | }
44 |
45 | /**
46 | * Try to determine the position of this collection in the supplied order.
47 | *
48 | * This goes over the variations and returns the index in $order of the
49 | * first variation that is found. If no variation is found in $order, null
50 | * is returned.
51 | *
52 | * Note: This strategy will not work when you want a different order for
53 | * different versions. When the properties/methods that result in the same
54 | * serialized name have to be ordered differently, we would need a
55 | * different approach. We would need to keep all properties separately - or
56 | * keep track of the PHP name and apply reordering after reducing.
57 | *
58 | * @param string[] $order Ordered list of property names (PHP code names, not serialized names)
59 | */
60 | public function getPosition(array $order): ?int
61 | {
62 | foreach ($this->variations as $variation) {
63 | $pos = array_search($variation->getName(), $order, true);
64 | if (false !== $pos) {
65 | return $pos;
66 | }
67 | }
68 |
69 | return null;
70 | }
71 |
72 | public function setSerializedName(string $name): void
73 | {
74 | $this->serializedName = self::serializedName($name);
75 | }
76 |
77 | public function getSerializedName(): string
78 | {
79 | return $this->serializedName;
80 | }
81 |
82 | public function addVariation(PropertyVariationMetadata $property): void
83 | {
84 | $this->variations[] = $property;
85 | }
86 |
87 | /**
88 | * @return PropertyVariationMetadata[]
89 | */
90 | public function getVariations(): array
91 | {
92 | return $this->variations;
93 | }
94 |
95 | public function hasVariation(string $name): bool
96 | {
97 | return null !== $this->findVariation($name);
98 | }
99 |
100 | /**
101 | * @param string $name The name of a PropertyVariation is its system name, e.g. for a virtual property the method name
102 | *
103 | * @throws \UnexpectedValueException if no variation with this name exists
104 | */
105 | public function getVariation(string $name): PropertyVariationMetadata
106 | {
107 | $property = $this->findVariation($name);
108 | if (null === $property) {
109 | throw new \UnexpectedValueException(sprintf('Property variation %s not found on PropertyCollection %s', $name, $this->serializedName));
110 | }
111 |
112 | return $property;
113 | }
114 |
115 | public function removeVariation(string $name): void
116 | {
117 | foreach ($this->variations as $i => $property) {
118 | if ($property->getName() === $name) {
119 | unset($this->variations[$i]);
120 | $this->variations = array_values($this->variations);
121 |
122 | break;
123 | }
124 | }
125 | }
126 |
127 | public function jsonSerialize(): array
128 | {
129 | return [
130 | 'serialized_name' => $this->serializedName,
131 | 'variations' => $this->variations,
132 | ];
133 | }
134 |
135 | private function findVariation(string $name): ?PropertyVariationMetadata
136 | {
137 | foreach ($this->variations as $property) {
138 | if ($property->getName() === $name) {
139 | return $property;
140 | }
141 | }
142 |
143 | return null;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/ModelParser/RawMetadata/PropertyVariationMetadata.php:
--------------------------------------------------------------------------------
1 | type = new PropertyTypeUnknown(true);
37 | $this->setPreferred($preferred);
38 | }
39 |
40 | public static function fromReflection(\ReflectionProperty $reflProperty): self
41 | {
42 | return new self($reflProperty->getName(), false, $reflProperty->isPublic());
43 | }
44 |
45 | public function setType(PropertyType $type): void
46 | {
47 | $this->type = $type;
48 | }
49 |
50 | public function getType(): PropertyType
51 | {
52 | return $this->type;
53 | }
54 |
55 | public function setPreferred(bool $preferred): void
56 | {
57 | $this->preferred = $preferred;
58 | }
59 |
60 | public function isPreferred(): bool
61 | {
62 | return $this->preferred;
63 | }
64 |
65 | public function setReadOnly(bool $readOnly): void
66 | {
67 | parent::setReadOnly($readOnly);
68 | }
69 |
70 | public function setPublic(bool $public): void
71 | {
72 | parent::setPublic($public);
73 | }
74 |
75 | public function setGroups(array $groups): void
76 | {
77 | parent::setGroups($groups);
78 | }
79 |
80 | public function setAccessor(PropertyAccessor $accessor): void
81 | {
82 | parent::setAccessor($accessor);
83 | }
84 |
85 | public function setVersionRange(VersionRange $version): void
86 | {
87 | parent::setVersionRange($version);
88 | }
89 |
90 | public function getMaxDepth(): ?int
91 | {
92 | return parent::getMaxDepth();
93 | }
94 |
95 | public function setMaxDepth(?int $maxDepth): void
96 | {
97 | parent::setMaxDepth($maxDepth);
98 | }
99 |
100 | /**
101 | * The value can be anything that the consumer understands.
102 | *
103 | * However, if it is an object, it should implement JsonSerializable to not
104 | * break debugging.
105 | */
106 | public function setCustomInformation(string $key, $value): void
107 | {
108 | parent::setCustomInformation($key, $value);
109 | }
110 |
111 | public function jsonSerialize(): array
112 | {
113 | $data = parent::jsonSerialize();
114 | $data['type'] = (string) $this->type;
115 |
116 | return $data;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/ModelParser/ReflectionParser.php:
--------------------------------------------------------------------------------
1 | typeParser = new PhpTypeParser();
30 | $this->reflectionSupportsPropertyType = version_compare(\PHP_VERSION, '7.4', '>=');
31 | }
32 |
33 | public function parse(RawClassMetadata $classMetadata): void
34 | {
35 | try {
36 | $reflClass = new \ReflectionClass($classMetadata->getClassName());
37 | } catch (\ReflectionException $e) {
38 | throw ParseException::classNotFound($classMetadata->getClassName(), $e);
39 | }
40 |
41 | $this->parseProperties($reflClass, $classMetadata);
42 | $this->parseConstructor($reflClass, $classMetadata);
43 | }
44 |
45 | private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
46 | {
47 | if ($reflParentClass = $reflClass->getParentClass()) {
48 | $this->parseProperties($reflParentClass, $classMetadata);
49 | }
50 |
51 | foreach ($reflClass->getProperties() as $reflProperty) {
52 | $type = null;
53 | $reflectionType = $this->reflectionSupportsPropertyType ? $reflProperty->getType() : null;
54 | if ($reflectionType instanceof \ReflectionNamedType) {
55 | // If the field has a union type (since PHP 8.0) or intersection type (since PHP 8.1),
56 | // the type would be a different kind of ReflectionType than ReflectionNamedType.
57 | // We don't have support in the metadata model to handle multiple types.
58 | $type = $this->typeParser->parseReflectionType($reflectionType);
59 | }
60 | if ($classMetadata->hasPropertyVariation($reflProperty->getName())) {
61 | $property = $classMetadata->getPropertyVariation($reflProperty->getName());
62 | $property->setPublic($reflProperty->isPublic());
63 | if ($type) {
64 | $property->setType($type);
65 | }
66 | } else {
67 | $property = PropertyVariationMetadata::fromReflection($reflProperty);
68 | if ($type) {
69 | $property->setType($type);
70 | }
71 | $classMetadata->addPropertyVariation($reflProperty->getName(), $property);
72 | }
73 | }
74 | }
75 |
76 | private function parseConstructor(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
77 | {
78 | $constructor = $reflClass->getConstructor();
79 | if (null === $constructor) {
80 | return;
81 | }
82 |
83 | foreach ($constructor->getParameters() as $reflParameter) {
84 | $classMetadata->addConstructorParameter(ParameterMetadata::fromReflection($reflParameter));
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/ModelParser/VisibilityAwarePropertyAccessGuesser.php:
--------------------------------------------------------------------------------
1 | getClassName());
28 | } catch (\ReflectionException $e) {
29 | throw ParseException::classNotFound($classMetadata->getClassName(), $e);
30 | }
31 |
32 | $this->parseProperties($reflClass, $classMetadata);
33 | }
34 |
35 | public function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
36 | {
37 | if ($reflParentClass = $reflClass->getParentClass()) {
38 | $this->parseProperties($reflParentClass, $classMetadata);
39 | }
40 |
41 | foreach ($reflClass->getProperties() as $property) {
42 | if ($property->isPublic() || !$classMetadata->hasPropertyVariation($property->getName())) {
43 | continue;
44 | }
45 |
46 | $variation = $classMetadata->getPropertyVariation($property->getName());
47 | $currentAccessor = $variation->getAccessor();
48 |
49 | if (!($currentAccessor->hasGetterMethod() && $currentAccessor->hasSetterMethod())) {
50 | $variation->setAccessor(new PropertyAccessor(
51 | $currentAccessor->getGetterMethod() ?: $this->guessGetter($reflClass, $variation),
52 | $currentAccessor->getSetterMethod() ?: $this->guessSetter($reflClass, $variation),
53 | ));
54 | }
55 | }
56 | }
57 |
58 | /**
59 | * Find a getter method for property, using prefixes `get`, `is`, or simply no prefix
60 | */
61 | private function guessGetter(\ReflectionClass $reflClass, PropertyVariationMetadata $variation): ?string
62 | {
63 | foreach (['get', 'is', ''] as $prefix) {
64 | $method = "{$prefix}{$variation->getName()}";
65 |
66 | if (!$reflClass->hasMethod($method)) {
67 | continue;
68 | }
69 |
70 | $reflMethod = $reflClass->getMethod($method);
71 |
72 | if ($reflMethod->isPublic() && (0 === $reflMethod->getNumberOfRequiredParameters())) {
73 | return $method;
74 | }
75 | }
76 |
77 | return null;
78 | }
79 |
80 | /**
81 | * Find a setter method for property, using prefix `set`, or simply no prefix
82 | */
83 | private function guessSetter(\ReflectionClass $reflClass, PropertyVariationMetadata $variation): ?string
84 | {
85 | foreach (['set', ''] as $prefix) {
86 | $method = "{$prefix}{$variation->getName()}";
87 |
88 | if (!$reflClass->hasMethod($method)) {
89 | continue;
90 | }
91 |
92 | $reflMethod = $reflClass->getMethod($method);
93 |
94 | if ($reflMethod->isPublic() && (1 === $reflMethod->getNumberOfRequiredParameters())) {
95 | return $method;
96 | }
97 | }
98 |
99 | return null;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Parser.php:
--------------------------------------------------------------------------------
1 | parsers = $parsers;
27 | }
28 |
29 | /**
30 | * @return RawClassMetadata[]
31 | *
32 | * @throws ParseException
33 | */
34 | public function parse(string $className): array
35 | {
36 | $registry = new RawClassMetadataRegistry();
37 |
38 | $this->parseModel($className, new ParserContext($className), $registry);
39 |
40 | return $registry->getAll();
41 | }
42 |
43 | private function parseModel(string $className, ParserContext $context, RawClassMetadataRegistry $registry): void
44 | {
45 | if ($registry->contains($className)) {
46 | return;
47 | }
48 |
49 | $rawClassMetadata = new RawClassMetadata($className);
50 | foreach ($this->parsers as $parser) {
51 | $parser->parse($rawClassMetadata);
52 | }
53 | $registry->add($rawClassMetadata);
54 |
55 | foreach ($rawClassMetadata->getPropertyVariations() as $property) {
56 | $type = $property->getType();
57 | if ($type instanceof PropertyTypeIterable) {
58 | $type = $type->getLeafType();
59 | }
60 | if ($type instanceof PropertyTypeClass) {
61 | $this->parseModel($type->getClassName(), $context->push($property), $registry);
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/PropertyReducer.php:
--------------------------------------------------------------------------------
1 | getPropertyCollections() as $propertyCollection) {
22 | $properties = [];
23 | foreach ($propertyCollection->getVariations() as $property) {
24 | $properties[] = $property;
25 | }
26 | foreach ($reducers as $reducer) {
27 | $properties = $reducer->reduce($propertyCollection->getSerializedName(), $properties);
28 | }
29 |
30 | if (\count($properties) > 0) {
31 | $classProperties[] = PropertyMetadata::fromRawProperty($propertyCollection->getSerializedName(), $properties[0]);
32 | }
33 | }
34 |
35 | return ClassMetadata::fromRawClassMetadata($rawClassMetadata, $classProperties);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/RawClassMetadataRegistry.php:
--------------------------------------------------------------------------------
1 | contains($classMetadata->getClassName())) {
19 | throw new \BadMethodCallException(sprintf('The model for "%s" is already in the registry', $classMetadata->getClassName()));
20 | }
21 |
22 | $this->classMetadata[$classMetadata->getClassName()] = $classMetadata;
23 | }
24 |
25 | public function contains(string $className): bool
26 | {
27 | return \array_key_exists($className, $this->classMetadata);
28 | }
29 |
30 | /**
31 | * @return RawClassMetadata[]
32 | */
33 | public function getAll(): array
34 | {
35 | return array_values($this->classMetadata);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/RecursionChecker.php:
--------------------------------------------------------------------------------
1 | variants (which themselves are products. You can configure to
23 | * abort at product->variants->variants to avoid a recursion.
24 | */
25 | final class RecursionChecker
26 | {
27 | /**
28 | * @var LoggerInterface|null
29 | */
30 | private $logger;
31 |
32 | /**
33 | * @var string[][]
34 | */
35 | private $expectedRecursions;
36 |
37 | /**
38 | * The expected recursions can be absolute, starting with the root class.
39 | * Or you can specify only a sub path. Both will be detected.
40 | *
41 | * A recursion configuration may contain Context::MATCH_EVERYTHING ('*') to
42 | * match every field at that level.
43 | *
44 | * Arrays and hashmaps are specified the same as single fields.
45 | *
46 | * @param string[][] $expectedRecursions List of expected recursions
47 | */
48 | public function __construct(LoggerInterface $logger = null, array $expectedRecursions = [])
49 | {
50 | $this->logger = $logger;
51 | $this->expectedRecursions = $expectedRecursions;
52 | }
53 |
54 | /**
55 | * @throws RecursionException
56 | */
57 | public function check(ClassMetadata $classMetadata): ClassMetadata
58 | {
59 | return $this->checkClassMetadata($classMetadata, new RecursionContext($classMetadata->getClassName()));
60 | }
61 |
62 | private function checkClassMetadata(ClassMetadata $classMetadata, RecursionContext $context): ClassMetadata
63 | {
64 | $propertiesToRemove = [];
65 |
66 | foreach ($classMetadata->getProperties() as $property) {
67 | $type = $property->getType();
68 | if ($type instanceof PropertyTypeIterable) {
69 | $type = $type->getLeafType();
70 | }
71 |
72 | if ($type instanceof PropertyTypeClass) {
73 | $propertyClassMetadata = $type->getClassMetadata();
74 | $propertyContext = $context->push($property);
75 |
76 | foreach ($this->expectedRecursions as $expectedRecursion) {
77 | /*
78 | * Future feature idea: The expected paths would work just the same if they where not about
79 | * recursions but general paths. We could move the check for circuit breaking outside of
80 | * the check whether we hit a recursion.
81 | */
82 | if ($propertyContext->matches($expectedRecursion)) {
83 | // Remove property of expected recursion
84 | if ($this->logger) {
85 | $this->logger->notice(
86 | 'Expected recursion found for class "{class_name}" in context {context}',
87 | [
88 | 'class_name' => $propertyClassMetadata->getClassName(),
89 | 'context' => (string) $propertyContext,
90 | ]
91 | );
92 | }
93 |
94 | $propertiesToRemove[] = $property->getName();
95 | continue 2;
96 | }
97 | }
98 |
99 | $maxDepth = $property->getMaxDepth();
100 | $stackCount = $propertyContext->countClassNames($classMetadata->getClassName());
101 | if (null === $maxDepth && $stackCount > 2) {
102 | throw RecursionException::forClass($propertyClassMetadata->getClassName(), $propertyContext);
103 | }
104 |
105 | if (null !== $maxDepth && $stackCount > $maxDepth) {
106 | return $classMetadata;
107 | }
108 |
109 | $type->setClassMetadata($this->checkClassMetadata($propertyClassMetadata, $propertyContext));
110 | }
111 | }
112 |
113 | if (0 === \count($propertiesToRemove)) {
114 | return $classMetadata;
115 | }
116 |
117 | return $classMetadata->withoutProperties($propertiesToRemove);
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/RecursionContext.php:
--------------------------------------------------------------------------------
1 | root = $root;
28 | }
29 |
30 | public function __toString(): string
31 | {
32 | if (0 === \count($this->stack)) {
33 | return $this->root;
34 | }
35 |
36 | $stack = array_map(static function (PropertyMetadata $propertyMetadata) {
37 | return $propertyMetadata->getSerializedName();
38 | }, $this->stack);
39 |
40 | return sprintf('%s->%s', $this->root, implode('->', $stack));
41 | }
42 |
43 | public function push(PropertyMetadata $property): self
44 | {
45 | $context = clone $this;
46 | $context->stack[] = $property;
47 |
48 | return $context;
49 | }
50 |
51 | /**
52 | * Check if we are at the specified stack.
53 | *
54 | * @see RecursionChecker
55 | *
56 | * @param string[] $stackToCheck List of optional root class and properties to go through
57 | */
58 | public function matches(array $stackToCheck): bool
59 | {
60 | if (0 === \count($stackToCheck) || \count($this->stack) + 1 < \count($stackToCheck)) {
61 | return false;
62 | }
63 |
64 | $current = [$this->root];
65 | foreach ($this->stack as $property) {
66 | $current[] = $property->getSerializedName();
67 | }
68 |
69 | foreach ($current as $i => $name) {
70 | if ($stackToCheck[0] === $name) {
71 | $valid = true;
72 | foreach ($stackToCheck as $j => $nameToCheck) {
73 | if (self::MATCH_EVERYTHING !== $nameToCheck && ($current[$i + (int) $j] ?? null) !== $nameToCheck) {
74 | $valid = false;
75 | break;
76 | }
77 | }
78 |
79 | if ($valid) {
80 | return true;
81 | }
82 | }
83 | }
84 |
85 | return false;
86 | }
87 |
88 | public function countClassNames(string $className): int
89 | {
90 | $count = 0;
91 | if ($this->root === $className) {
92 | ++$count;
93 | }
94 | foreach ($this->stack as $property) {
95 | $type = $property->getType();
96 | if ($type instanceof PropertyTypeIterable) {
97 | $type = $type->getLeafType();
98 | }
99 | if ($type instanceof PropertyTypeClass && $type->getClassName() === $className) {
100 | ++$count;
101 | }
102 | }
103 |
104 | return $count;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/Reducer/GroupReducer.php:
--------------------------------------------------------------------------------
1 | groups = $groups;
25 | }
26 |
27 | public function reduce(string $serializedName, array $properties): array
28 | {
29 | $includedProperties = [];
30 | foreach ($properties as $property) {
31 | if ($this->includeProperty($property)) {
32 | $includedProperties[] = $property;
33 | }
34 | }
35 |
36 | return $includedProperties;
37 | }
38 |
39 | private function includeProperty(PropertyVariationMetadata $property): bool
40 | {
41 | return 0 === \count($this->groups) || 0 < \count(array_intersect($property->getGroups(), $this->groups));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Reducer/PreferredReducer.php:
--------------------------------------------------------------------------------
1 | isPreferred();
21 | }));
22 |
23 | if (\count($preferred)) {
24 | return $preferred;
25 | }
26 |
27 | return $properties;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Reducer/PropertyReducerInterface.php:
--------------------------------------------------------------------------------
1 | getName()) {
23 | return -1;
24 | }
25 | if ($serializedName === $propertyB->getName()) {
26 | return 1;
27 | }
28 |
29 | return 0;
30 | });
31 |
32 | return $properties;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Reducer/VersionReducer.php:
--------------------------------------------------------------------------------
1 | version = $version;
20 | }
21 |
22 | public function reduce(string $serializedName, array $properties): array
23 | {
24 | $includedProperties = [];
25 | foreach ($properties as $property) {
26 | if ($property->getVersionRange()->isIncluded($this->version)) {
27 | $includedProperties[] = $property;
28 | }
29 | }
30 |
31 | return $includedProperties;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/TypeParser/JMSTypeParser.php:
--------------------------------------------------------------------------------
1 | jmsTypeParser = new Parser();
33 | }
34 |
35 | public function parse(string $rawType): PropertyType
36 | {
37 | if ('' === $rawType) {
38 | return new PropertyTypeUnknown(true);
39 | }
40 |
41 | return $this->parseType($this->jmsTypeParser->parse($rawType));
42 | }
43 |
44 | private function parseType(array $typeInfo, bool $isSubType = false): PropertyType
45 | {
46 | $typeInfo = array_merge(
47 | [
48 | 'name' => null,
49 | 'params' => [],
50 | ],
51 | $typeInfo
52 | );
53 |
54 | // JMS types are nullable except if it's a sub type (part of array)
55 | $nullable = !$isSubType;
56 |
57 | if (0 === \count($typeInfo['params'])) {
58 | if (self::TYPE_ARRAY === $typeInfo['name']) {
59 | return new PropertyTypeIterable(new PropertyTypeUnknown(false), false, $nullable);
60 | }
61 |
62 | if (PropertyTypePrimitive::isTypePrimitive($typeInfo['name'])) {
63 | return new PropertyTypePrimitive($typeInfo['name'], $nullable);
64 | }
65 | if (PropertyTypeDateTime::isTypeDateTime($typeInfo['name'])) {
66 | return PropertyTypeDateTime::fromDateTimeClass($typeInfo['name'], $nullable);
67 | }
68 |
69 | return new PropertyTypeClass($typeInfo['name'], $nullable);
70 | }
71 |
72 | $collectionClass = $this->getCollectionClass($typeInfo['name']);
73 | if (self::TYPE_ARRAY === $typeInfo['name'] || $collectionClass) {
74 | if (1 === \count($typeInfo['params'])) {
75 | return new PropertyTypeIterable($this->parseType($typeInfo['params'][0], true), false, $nullable, $collectionClass);
76 | }
77 | if (2 === \count($typeInfo['params'])) {
78 | return new PropertyTypeIterable($this->parseType($typeInfo['params'][1], true), true, $nullable, $collectionClass);
79 | }
80 |
81 | throw new InvalidTypeException(sprintf('JMS property type array can\'t have more than 2 parameters (%s)', var_export($typeInfo, true)));
82 | }
83 |
84 | if (PropertyTypeDateTime::isTypeDateTime($typeInfo['name']) || (self::TYPE_DATETIME_INTERFACE === $typeInfo['name'])) {
85 | // the case of datetime without params is already handled above, we know we have params
86 | $serializeFormat = $typeInfo['params'][0] ?: null;
87 | // {@link \JMS\Serializer\Handler\DateHandler} of jms/serializer defaults to using the serialization format as a deserialization format if none was supplied...
88 | $deserializeFormats = ($typeInfo['params'][2] ?? null) ?: $serializeFormat;
89 | // ... and always converts single strings to arrays
90 | $deserializeFormats = \is_string($deserializeFormats) ? [$deserializeFormats] : $deserializeFormats;
91 | // Jms defaults to DateTime when given DateTimeInterface despite the documentation saying DateTimeImmutable, {@see \JMS\Serializer\Handler\DateHandler} in jms/serializer
92 | $className = (self::TYPE_DATETIME_INTERFACE === $typeInfo['name']) ? \DateTime::class : $typeInfo['name'];
93 |
94 | return PropertyTypeDateTime::fromDateTimeClass(
95 | $className,
96 | $nullable,
97 | new DateTimeOptions(
98 | $serializeFormat,
99 | ($typeInfo['params'][1] ?? null) ?: null,
100 | $deserializeFormats,
101 | )
102 | );
103 | }
104 |
105 | throw new InvalidTypeException(sprintf('Unknown JMS property found (%s)', var_export($typeInfo, true)));
106 | }
107 |
108 | private function getCollectionClass(string $name): ?string
109 | {
110 | switch ($name) {
111 | case self::TYPE_ARRAY_COLLECTION:
112 | return ArrayCollection::class;
113 | default:
114 | return is_a($name, Collection::class, true) ? $name : null;
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/TypeParser/PhpTypeParser.php:
--------------------------------------------------------------------------------
1 | useStatementsParser = new PhpParser();
39 | }
40 |
41 | /**
42 | * @throws InvalidTypeException if an invalid type or multiple types were defined
43 | */
44 | public function parseAnnotationType(string $rawType, \ReflectionClass $declaringClass): PropertyType
45 | {
46 | if ('' === $rawType) {
47 | return new PropertyTypeUnknown(true);
48 | }
49 |
50 | $types = [];
51 | $nullable = false;
52 | foreach (explode(self::TYPE_SEPARATOR, $rawType) as $part) {
53 | if (self::TYPE_NULL === $part || self::TYPE_MIXED === $part) {
54 | $nullable = true;
55 | } elseif (!\in_array($part, self::TYPES_GENERIC, true)) {
56 | $types[] = $part;
57 | }
58 | }
59 |
60 | $collectionClass = null;
61 | $filteredTypes = [];
62 | foreach ($types as $type) {
63 | $resolvedClass = $this->resolveClass($type, $declaringClass);
64 | if (is_a($resolvedClass, Collection::class, true)) {
65 | $collectionClass = $resolvedClass;
66 | } else {
67 | $filteredTypes[] = $type;
68 | }
69 | }
70 |
71 | if (0 === \count($filteredTypes)) {
72 | return new PropertyTypeUnknown($nullable);
73 | }
74 | if (\count($filteredTypes) > 1) {
75 | throw new InvalidTypeException(sprintf('Multiple types are not supported (%s)', $rawType));
76 | }
77 |
78 | return $this->createType($filteredTypes[0], $nullable, $declaringClass, $collectionClass);
79 | }
80 |
81 | /**
82 | * @throws InvalidTypeException if an invalid type was defined
83 | */
84 | public function parseReflectionType(\ReflectionType $reflType): PropertyType
85 | {
86 | if ($reflType instanceof \ReflectionNamedType) {
87 | return $this->createType($reflType->getName(), $reflType->allowsNull());
88 | }
89 |
90 | throw new InvalidTypeException(sprintf('No type information found, got %s but expected %s', \ReflectionType::class, \ReflectionNamedType::class));
91 | }
92 |
93 | private function createType(string $rawType, bool $nullable, \ReflectionClass $reflClass = null, string $collectionClass = null): PropertyType
94 | {
95 | if (self::TYPE_ARRAY === $rawType) {
96 | return new PropertyTypeIterable(new PropertyTypeUnknown(false), false, $nullable);
97 | }
98 |
99 | if (self::TYPE_ARRAY_SUFFIX === substr($rawType, -\strlen(self::TYPE_ARRAY_SUFFIX))) {
100 | $rawSubType = substr($rawType, 0, \strlen($rawType) - \strlen(self::TYPE_ARRAY_SUFFIX));
101 |
102 | return new PropertyTypeIterable($this->createType($rawSubType, false, $reflClass), false, $nullable, $collectionClass);
103 | }
104 | if (self::TYPE_HASHMAP_SUFFIX === substr($rawType, -\strlen(self::TYPE_HASHMAP_SUFFIX))) {
105 | $rawSubType = substr($rawType, 0, \strlen($rawType) - \strlen(self::TYPE_HASHMAP_SUFFIX));
106 |
107 | return new PropertyTypeIterable($this->createType($rawSubType, false, $reflClass), true, $nullable, $collectionClass);
108 | }
109 |
110 | if (self::TYPE_RESOURCE === $rawType) {
111 | throw new InvalidTypeException('Type "resource" is not supported');
112 | }
113 |
114 | if (PropertyTypePrimitive::isTypePrimitive($rawType)) {
115 | return new PropertyTypePrimitive($rawType, $nullable);
116 | }
117 |
118 | $resolvedClass = $this->resolveClass($rawType, $reflClass);
119 |
120 | if (PropertyTypeDateTime::isTypeDateTime($resolvedClass)) {
121 | return PropertyTypeDateTime::fromDateTimeClass($resolvedClass, $nullable);
122 | }
123 |
124 | return new PropertyTypeClass($resolvedClass, $nullable);
125 | }
126 |
127 | private function resolveClass(string $className, \ReflectionClass $reflClass = null): string
128 | {
129 | // leading backslash means absolute class name
130 | if (0 === strpos($className, '\\')) {
131 | return substr($className, 1);
132 | }
133 |
134 | if (null !== $reflClass) {
135 | // resolve use statements of the class with the type information
136 | $lowerClassName = strtolower($className);
137 |
138 | $reflCurrentClass = $reflClass;
139 | do {
140 | $imports = $this->useStatementsParser->parseUseStatements($reflCurrentClass);
141 | if (isset($imports[$lowerClassName])) {
142 | return $imports[$lowerClassName];
143 | }
144 | } while (false !== ($reflCurrentClass = $reflCurrentClass->getParentClass()));
145 |
146 | foreach ($reflClass->getTraits() as $reflTrait) {
147 | $imports = $this->useStatementsParser->parseUseStatements($reflTrait);
148 | if (isset($imports[$lowerClassName])) {
149 | return $imports[$lowerClassName];
150 | }
151 | }
152 |
153 | // the referenced class is expected to be in the same namespace
154 | $namespace = $reflClass->getNamespaceName();
155 | if ('' !== $namespace) {
156 | return $namespace.'\\'.$className;
157 | }
158 | }
159 |
160 | // edge case of models defined in the global namespace
161 | return $className;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/tests/BuilderTest.php:
--------------------------------------------------------------------------------
1 | builder = new Builder(
39 | $parser,
40 | new RecursionChecker($this->createMock(LoggerInterface::class))
41 | );
42 | }
43 |
44 | public function testBuild(): void
45 | {
46 | $c = new class() {
47 | /**
48 | * @var Nested
49 | */
50 | private $property;
51 | };
52 |
53 | $classMetadata = $this->builder->build(\get_class($c));
54 |
55 | $props = $classMetadata->getProperties();
56 | $this->assertCount(1, $props, 'Number of properties should match');
57 |
58 | $this->assertProperty('property', 'property', false, false, $props[0]);
59 | $this->assertPropertyType($props[0]->getType(), PropertyTypeClass::class, Nested::class, false);
60 |
61 | $type = $props[0]->getType();
62 | $this->assertInstanceOf(PropertyTypeClass::class, $type);
63 | $nestedMetadata = $type->getClassMetadata();
64 | $props = $nestedMetadata->getProperties();
65 | $this->assertCount(1, $props, 'Number of properties should match');
66 | $this->assertProperty('nestedProperty', 'nested_property', false, false, $props[0]);
67 | }
68 |
69 | private function assertProperty(string $name, string $serializedName, bool $public, bool $readOnly, PropertyMetadata $property): void
70 | {
71 | $this->assertSame($name, $property->getName(), 'Name of property should match');
72 | $this->assertSame($serializedName, $property->getSerializedName(), "Serialized name of property {$name} should match");
73 | $this->assertSame($public, $property->isPublic(), "Public flag of property {$name} should match");
74 | $this->assertSame($readOnly, $property->isReadOnly(), "Read only flag of property {$name} should match");
75 | }
76 |
77 | private function assertPropertyType(PropertyType $type, string $propertyTypeClass, string $typeString, bool $nullable): void
78 | {
79 | $this->assertInstanceOf($propertyTypeClass, $type);
80 | $this->assertSame($nullable, $type->isNullable());
81 | $this->assertSame($typeString, (string) $type);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/tests/Metadata/ClassMetadataTest.php:
--------------------------------------------------------------------------------
1 | addConstructorParameter(new ParameterMetadata('arg', true));
22 | $rawClassMetadata->addConstructorParameter(new ParameterMetadata('withDefault', false, 'default'));
23 | $rawClassMetadata->addConstructorParameter(new ParameterMetadata('optional', false));
24 | $rawClassMetadata->addPostDeserializeMethod('postDeserialize');
25 | $properties = [new PropertyMetadata('test', 'testProperty')];
26 |
27 | $classMetadata = ClassMetadata::fromRawClassMetadata($rawClassMetadata, $properties);
28 | $this->assertSame('Foo', $classMetadata->getClassName());
29 |
30 | $constructorParameters = $classMetadata->getConstructorParameters();
31 | $this->assertCount(3, $constructorParameters);
32 | $this->assertSame('arg', $constructorParameters[0]->getName());
33 | $this->assertSame('withDefault', $constructorParameters[1]->getName());
34 | $this->assertSame('optional', $constructorParameters[2]->getName());
35 |
36 | $postDeserializeMethods = $classMetadata->getPostDeserializeMethods();
37 | $this->assertCount(1, $postDeserializeMethods);
38 | $this->assertSame('postDeserialize', $postDeserializeMethods[0]);
39 |
40 | $props = $classMetadata->getProperties();
41 | $this->assertCount(1, $props);
42 | $this->assertSame('testProperty', $props[0]->getName());
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Metadata/PropertyTypeTest.php:
--------------------------------------------------------------------------------
1 | merge($typeB);
106 |
107 | $this->assertSame($expectedType, (string) $result);
108 | $this->assertSame($expectedNullable, $result->isNullable(), 'Nullable flag should match');
109 | }
110 |
111 | /**
112 | * Special case: array to hashmap is allowed. hashmap to array is not.
113 | */
114 | public function testUpgradeToHashmap(): void
115 | {
116 | $array = new PropertyTypeIterable(new PropertyTypePrimitive('bool', false), false, true);
117 | $hashmap = new PropertyTypeIterable(new PropertyTypePrimitive('bool', false), true, true);
118 |
119 | /** @var PropertyTypeIterable $merged */
120 | $merged = $array->merge($hashmap);
121 | $this->assertInstanceOf(PropertyTypeIterable::class, $merged);
122 | $this->assertTrue($merged->isNullable());
123 | $this->assertTrue($merged->isHashmap());
124 | /** @var PropertyTypePrimitive $inner */
125 | $inner = $merged->getSubType();
126 | $this->assertInstanceOf(PropertyTypePrimitive::class, $inner);
127 | $this->assertSame('bool', $inner->getTypeName());
128 |
129 | $this->expectException(\UnexpectedValueException::class);
130 | $hashmap->merge($array);
131 | }
132 |
133 | public function testMergeInvalidTypes(): void
134 | {
135 | $types = $this->getDifferentTypes();
136 |
137 | foreach ($types as $typeA) {
138 | foreach ($types as $typeB) {
139 | if ($typeA === $typeB || $typeB instanceof PropertyTypeUnknown) {
140 | continue;
141 | }
142 |
143 | try {
144 | $typeA->merge($typeB);
145 | $this->fail(sprintf('Merge of %s into %s should not be possible', (string) $typeB, (string) $typeA));
146 | } catch (\UnexpectedValueException $e) {
147 | $this->assertStringContainsString('merge', $e->getMessage());
148 | }
149 | }
150 | }
151 | }
152 |
153 | /**
154 | * @return PropertyType[]
155 | */
156 | private function getDifferentTypes(): array
157 | {
158 | return [
159 | new PropertyTypeUnknown(true),
160 | new PropertyTypePrimitive('string', true),
161 | new PropertyTypePrimitive('int', true),
162 | new PropertyTypeDateTime(false, true),
163 | new PropertyTypeDateTime(true, true),
164 | new PropertyTypeClass(\stdClass::class, true),
165 | new PropertyTypeIterable(new PropertyTypePrimitive('bool', false), false, true),
166 | new PropertyTypeIterable(new PropertyTypePrimitive('int', false), false, true),
167 | new PropertyTypeIterable(new PropertyTypePrimitive('string', false), true, true),
168 | ];
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/tests/Metadata/VersionRangeTest.php:
--------------------------------------------------------------------------------
1 | assertNull($versionRange->getSince());
19 | $this->assertNull($versionRange->getUntil());
20 | }
21 |
22 | public function testDefined(): void
23 | {
24 | $versionRange = new VersionRange(null, null);
25 | $this->assertFalse($versionRange->isDefined());
26 |
27 | $versionRange = new VersionRange('1', null);
28 | $this->assertTrue($versionRange->isDefined());
29 |
30 | $versionRange = new VersionRange(null, '1');
31 | $this->assertTrue($versionRange->isDefined());
32 | }
33 |
34 | public function testWithSince(): void
35 | {
36 | $version = new VersionRange('1', '2');
37 | $new = $version->withSince('2');
38 | $this->assertNotSame($version, $new);
39 | $this->assertSame('2', $new->getSince());
40 | $this->assertSame('1', $version->getSince());
41 | }
42 |
43 | public function testWithUntil(): void
44 | {
45 | $version = new VersionRange('1', '2');
46 | $new = $version->withUntil('3');
47 | $this->assertNotSame($version, $new);
48 | $this->assertSame('3', $new->getUntil());
49 | $this->assertSame('2', $version->getUntil());
50 | }
51 |
52 | public function provideIsIncludedCases(): iterable
53 | {
54 | return [
55 | 'null is lowest and highest' => [
56 | new VersionRange(null, null),
57 | '0',
58 | true,
59 | ],
60 | 'lower bound' => [
61 | new VersionRange('0', '1'),
62 | '0',
63 | true,
64 | ],
65 | 'somewhere' => [
66 | new VersionRange('1', '2'),
67 | '1',
68 | true,
69 | ],
70 | 'upper bound' => [
71 | new VersionRange('1', '2'),
72 | '2',
73 | true,
74 | ],
75 | 'below' => [
76 | new VersionRange('2', '3'),
77 | '1',
78 | false,
79 | ],
80 | 'above' => [
81 | new VersionRange('1', '2'),
82 | '4',
83 | false,
84 | ],
85 | ];
86 | }
87 |
88 | /**
89 | * @dataProvider provideIsIncludedCases
90 | */
91 | public function testIsIncluded(VersionRange $versionRange, string $version, bool $expected): void
92 | {
93 | $this->assertSame($expected, $versionRange->isIncluded($version));
94 | }
95 |
96 | public function provideAllowsLowerThanCases(): iterable
97 | {
98 | return [
99 | 'same null' => [
100 | new VersionRange(null, null),
101 | new VersionRange(null, null),
102 | false,
103 | ],
104 | 'same value' => [
105 | new VersionRange('1', null),
106 | new VersionRange('1', null),
107 | false,
108 | ],
109 | 'null is lowest' => [
110 | new VersionRange(null, null),
111 | new VersionRange('1', null),
112 | true,
113 | ],
114 | 'other null is lowest' => [
115 | new VersionRange('1', null),
116 | new VersionRange(null, null),
117 | false,
118 | ],
119 | 'first is lower' => [
120 | new VersionRange('1', null),
121 | new VersionRange('2', null),
122 | true,
123 | ],
124 | 'second is lower' => [
125 | new VersionRange('2', null),
126 | new VersionRange('1', null),
127 | false,
128 | ],
129 | ];
130 | }
131 |
132 | /**
133 | * @dataProvider provideAllowsLowerThanCases
134 | */
135 | public function testAllowsLowerThan(VersionRange $versionRange, VersionRange $other, bool $lower): void
136 | {
137 | $this->assertSame($lower, $versionRange->allowsLowerThan($other));
138 | }
139 |
140 | public function provideAllowsHigherThanCases(): iterable
141 | {
142 | return [
143 | 'same null' => [
144 | new VersionRange(null, null),
145 | new VersionRange(null, null),
146 | false,
147 | ],
148 | 'same value' => [
149 | new VersionRange(null, '1'),
150 | new VersionRange(null, '1'),
151 | false,
152 | ],
153 | 'null is highest' => [
154 | new VersionRange(null, null),
155 | new VersionRange(null, '1'),
156 | false,
157 | ],
158 | 'other null is highest' => [
159 | new VersionRange(null, '1'),
160 | new VersionRange(null, null),
161 | true,
162 | ],
163 | 'second value is higher' => [
164 | new VersionRange(null, '1'),
165 | new VersionRange(null, '2'),
166 | false,
167 | ],
168 | 'first value is higher' => [
169 | new VersionRange(null, '2'),
170 | new VersionRange(null, '1'),
171 | true,
172 | ],
173 | ];
174 | }
175 |
176 | /**
177 | * @dataProvider provideAllowsHigherThanCases
178 | */
179 | public function testAllowsHigherThan(VersionRange $versionRange, VersionRange $other, bool $higher): void
180 | {
181 | $this->assertSame($higher, $versionRange->allowsHigherThan($other));
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/tests/ModelParser/Fixtures/IntersectionTypeDeclarationModel.php:
--------------------------------------------------------------------------------
1 | parser->parse($classMetadata);
28 |
29 | $props = $classMetadata->getPropertyCollections();
30 | $this->assertCount(1, $props, 'Number of properties should match');
31 |
32 | $this->assertPropertyCollection('property', 1, $props[0]);
33 | $this->assertPropertyVariation('property', false, true, $props[0]->getVariations()[0]);
34 | }
35 |
36 | public function testAttributes(): void
37 | {
38 | $c = new class() {
39 | #[JMS\Type('string')]
40 | private $property1;
41 |
42 | #[JMS\Type('bool')]
43 | public $property2;
44 | };
45 |
46 | $classMetadata = new RawClassMetadata(\get_class($c));
47 | $this->parser->parse($classMetadata);
48 |
49 | $props = $classMetadata->getPropertyCollections();
50 | $this->assertCount(2, $props, 'Number of properties should match');
51 |
52 | $this->assertPropertyCollection('property1', 1, $props[0]);
53 | $property = $props[0]->getVariations()[0];
54 | $this->assertPropertyVariation('property1', false, false, $property);
55 | $this->assertPropertyType(PropertyTypePrimitive::class, 'string|null', true, $property->getType());
56 |
57 | $this->assertPropertyCollection('property2', 1, $props[1]);
58 | $property = $props[1]->getVariations()[0];
59 | $this->assertPropertyVariation('property2', true, false, $property);
60 | $this->assertPropertyType(PropertyTypePrimitive::class, 'bool|null', true, $property->getType());
61 | }
62 |
63 | public function testAttributesMixedWithAnnotations(): void
64 | {
65 | $c = new class() {
66 | /**
67 | * @JMS\SerializedName("property_mixed")
68 | *
69 | * @JMS\Groups({"group1"})
70 | */
71 | #[JMS\Type('string')]
72 | private $mixedProperty;
73 |
74 | #[JMS\SerializedName('property_attribute')]
75 | #[JMS\Type('bool')]
76 | public $attributeProperty;
77 |
78 | /**
79 | * @JMS\Type("array")
80 | */
81 | public $annotationsProperty;
82 | };
83 |
84 | $classMetadata = new RawClassMetadata(\get_class($c));
85 | $this->parser->parse($classMetadata);
86 |
87 | $props = $classMetadata->getPropertyCollections();
88 | $this->assertCount(3, $props, 'Number of properties should match');
89 |
90 | $this->assertPropertyCollection('property_mixed', 1, $props[0]);
91 | $property = $props[0]->getVariations()[0];
92 | $this->assertPropertyVariation('mixedProperty', false, false, $property);
93 | $this->assertPropertyType(PropertyTypePrimitive::class, 'string|null', true, $property->getType());
94 | $this->assertSame(['group1'], $props[0]->getVariations()[0]->getGroups());
95 |
96 | $this->assertPropertyCollection('property_attribute', 1, $props[1]);
97 | $property = $props[1]->getVariations()[0];
98 | $this->assertPropertyVariation('attributeProperty', true, false, $property);
99 | $this->assertPropertyType(PropertyTypePrimitive::class, 'bool|null', true, $property->getType());
100 |
101 | $this->assertPropertyCollection('annotations_property', 1, $props[2]);
102 | $property = $props[2]->getVariations()[0];
103 | $this->assertPropertyVariation('annotationsProperty', true, false, $property);
104 | $this->assertPropertyType(PropertyTypeIterable::class, 'string[]|null', true, $property->getType());
105 | }
106 |
107 | public function testVirtualPropertyWithoutDocblock(): void
108 | {
109 | $c = new class() {
110 | #[JMS\VirtualProperty]
111 | public function foo(): string
112 | {
113 | return 'bar';
114 | }
115 | };
116 |
117 | $classMetadata = new RawClassMetadata(\get_class($c));
118 | $this->parser->parse($classMetadata);
119 |
120 | $props = $classMetadata->getPropertyCollections();
121 | $this->assertCount(1, $props, 'Number of properties should match');
122 |
123 | $this->assertPropertyCollection('foo', 1, $props[0]);
124 | $property = $props[0]->getVariations()[0];
125 | $this->assertPropertyVariation('foo', true, true, $property);
126 | $this->assertPropertyType(PropertyTypePrimitive::class, 'string', false, $property->getType());
127 | $this->assertPropertyAccessor('foo', null, $property->getAccessor());
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/tests/ModelParser/JMSParserTestLegacy.php:
--------------------------------------------------------------------------------
1 | parser->parse($classMetadata);
26 |
27 | $props = $classMetadata->getPropertyCollections();
28 | $this->assertCount(1, $props, 'Number of properties should match');
29 |
30 | $this->assertPropertyCollection('property', 1, $props[0]);
31 | $this->assertPropertyVariation('property', false, true, $props[0]->getVariations()[0]);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/ModelParser/Model/AbstractModel.php:
--------------------------------------------------------------------------------
1 |
30 | */
31 | private $collectionNamespace;
32 | }
33 |
--------------------------------------------------------------------------------
/tests/ModelParser/ParserContextTest.php:
--------------------------------------------------------------------------------
1 | assertStringContainsString('Root', $s);
22 | $this->assertStringNotContainsString('property1', $s);
23 | $this->assertStringNotContainsString('property2', $s);
24 | }
25 |
26 | public function testPush(): void
27 | {
28 | $context = new ParserContext('Root');
29 | $context = $context->push(new PropertyVariationMetadata('property1', true, true));
30 | $context = $context->push(new PropertyVariationMetadata('property2', false, true));
31 |
32 | $s = (string) $context;
33 | $this->assertStringContainsString('Root', $s);
34 | $this->assertStringContainsString('property1', $s);
35 | $this->assertStringContainsString('property2', $s);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/ModelParser/RawMetadata/RawClassMetadataTest.php:
--------------------------------------------------------------------------------
1 | addPropertyVariation('test', new PropertyVariationMetadata('testProperty', true, false));
20 | $collection = $rawClassMetadata->getPropertyCollection('test');
21 |
22 | $this->assertCount(1, $collection->getVariations());
23 | $this->assertSame('test', $collection->getSerializedName());
24 |
25 | $rawClassMetadata->renameProperty('test', 'new_name');
26 | $this->assertFalse($rawClassMetadata->hasPropertyCollection('test'));
27 | $this->assertTrue($rawClassMetadata->hasPropertyCollection('new_name'));
28 | $collection = $rawClassMetadata->getPropertyCollection('new_name');
29 | $this->assertCount(1, $collection->getVariations());
30 | $this->assertSame('new_name', $collection->getSerializedName());
31 |
32 | $this->assertTrue($collection->hasVariation('testProperty'));
33 | $variation = $collection->getVariation('testProperty');
34 | $this->assertSame('testProperty', $variation->getName());
35 | }
36 |
37 | public function testRenameMerge(): void
38 | {
39 | $rawClassMetadata = new RawClassMetadata('Foo');
40 | $rawClassMetadata->addPropertyVariation('testProperty', new PropertyVariationMetadata('testProperty', true, false));
41 | $rawClassMetadata->addPropertyVariation('test', new PropertyVariationMetadata('test', true, false));
42 |
43 | $this->assertTrue($rawClassMetadata->hasPropertyCollection('testProperty'));
44 | $this->assertTrue($rawClassMetadata->hasPropertyCollection('test'));
45 |
46 | $rawClassMetadata->renameProperty('testProperty', 'test');
47 | $this->assertFalse($rawClassMetadata->hasPropertyCollection('testProperty'));
48 | $this->assertTrue($rawClassMetadata->hasPropertyCollection('test'));
49 | $collection = $rawClassMetadata->getPropertyCollection('test');
50 | $this->assertCount(2, $collection->getVariations());
51 | $this->assertSame('test', $collection->getSerializedName());
52 |
53 | $this->assertTrue($collection->hasVariation('testProperty'));
54 | $variation = $collection->getVariation('testProperty');
55 | $this->assertSame('testProperty', $variation->getName());
56 | $this->assertTrue($collection->hasVariation('test'));
57 | $variation = $collection->getVariation('test');
58 | $this->assertSame('test', $variation->getName());
59 | }
60 |
61 | public function testRemove(): void
62 | {
63 | $rawClassMetadata = new RawClassMetadata('Foo');
64 | $rawClassMetadata->addPropertyVariation('test', new PropertyVariationMetadata('testProperty', true, false));
65 |
66 | $this->assertCount(1, $rawClassMetadata->getPropertyCollections());
67 | $this->assertTrue($rawClassMetadata->hasPropertyVariation('testProperty'));
68 | $this->assertTrue($rawClassMetadata->hasPropertyCollection('test'));
69 |
70 | $rawClassMetadata->removePropertyVariation('testProperty');
71 |
72 | $this->assertCount(0, $rawClassMetadata->getPropertyCollections());
73 | $this->assertFalse($rawClassMetadata->hasPropertyVariation('testProperty'));
74 | $this->assertFalse($rawClassMetadata->hasPropertyCollection('test'));
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tests/ModelParser/VisibilityAwarePropertyAccessGuesserTest.php:
--------------------------------------------------------------------------------
1 | parseClass($class, $parsers);
22 |
23 | $this->assertSame(\get_class($class), $classMetadata->getClassName());
24 | $this->assertCount($expectedPropertyCount, $classMetadata->getPropertyCollections(), 'Number of properties should match');
25 |
26 | if (null !== $accessType) {
27 | ['public' => $public, 'hasGetter' => $hasGetter, 'hasSetter' => $hasSetter] = $accessType;
28 |
29 | foreach ($classMetadata->getPropertyVariations() as $propertyVariation) {
30 | $this->assertSame($public, $propertyVariation->isPublic());
31 |
32 | if (null !== $hasGetter) {
33 | $this->assertSame($hasGetter, $propertyVariation->getAccessor()->hasGetterMethod());
34 | }
35 | if (null !== $hasSetter) {
36 | $this->assertSame($hasSetter, $propertyVariation->getAccessor()->hasSetterMethod());
37 | }
38 | }
39 | }
40 | }
41 |
42 | /**
43 | * @param class-string|object $class
44 | * @param ModelParserInterface[] $parsers
45 | */
46 | public function parseClass($class, array $parsers): RawClassMetadata
47 | {
48 | $class = \is_object($class) ? \get_class($class) : $class;
49 | $classMetadata = new RawClassMetadata($class);
50 |
51 | foreach ($parsers as $parser) {
52 | $parser->parse($classMetadata);
53 | }
54 |
55 | return $classMetadata;
56 | }
57 |
58 | /**
59 | * @return Generator
69 | */
70 | public static function provideClassesTests(): \Generator
71 | {
72 | yield 'NoPredecessor' => [
73 | 'class' => new class() {
74 | public ?string $name = 'php';
75 | },
76 | 'parsers' => [
77 | new VisibilityAwarePropertyAccessGuesser(),
78 | ],
79 | 'expectedPropertyCount' => 0,
80 | 'accessType' => null,
81 | ];
82 | yield 'Empty' => [
83 | 'class' => new class() {
84 | },
85 | 'parsers' => [
86 | new ReflectionParser(),
87 | new VisibilityAwarePropertyAccessGuesser(),
88 | ],
89 | 'expectedPropertyCount' => 0,
90 | 'accessType' => null,
91 | ];
92 | yield 'SinglePublic' => [
93 | 'class' => new class() {
94 | public ?string $name = 'php';
95 | },
96 | 'parsers' => [
97 | new ReflectionParser(),
98 | new VisibilityAwarePropertyAccessGuesser(),
99 | ],
100 | 'expectedPropertyCount' => 1,
101 | 'accessType' => [
102 | 'public' => true,
103 | 'hasGetter' => null,
104 | 'hasSetter' => null,
105 | ],
106 | ];
107 | yield 'SinglePrivate' => [
108 | 'class' => new class() {
109 | private ?string $name = 'php';
110 |
111 | public function getName(): ?string
112 | {
113 | return $this->name;
114 | }
115 |
116 | public function setName(?string $name): void
117 | {
118 | $this->name = $name;
119 | }
120 | },
121 | 'parsers' => [
122 | new ReflectionParser(),
123 | new VisibilityAwarePropertyAccessGuesser(),
124 | ],
125 | 'expectedPropertyCount' => 1,
126 | 'accessType' => [
127 | 'public' => false,
128 | 'hasGetter' => true,
129 | 'hasSetter' => true,
130 | ],
131 | ];
132 | yield 'MissingGetter' => [
133 | 'class' => new class() {
134 | private ?string $name;
135 |
136 | public function setName(?string $name): void
137 | {
138 | $this->name = $name;
139 | }
140 | },
141 | 'parsers' => [
142 | new ReflectionParser(),
143 | new VisibilityAwarePropertyAccessGuesser(),
144 | ],
145 | 'expectedPropertyCount' => 1,
146 | 'accessType' => [
147 | 'public' => false,
148 | 'hasGetter' => false,
149 | 'hasSetter' => true,
150 | ],
151 | ];
152 | yield 'MissingSetter' => [
153 | 'class' => new class() {
154 | private ?string $name = 'php';
155 |
156 | public function getName(): ?string
157 | {
158 | return $this->name;
159 | }
160 | },
161 | 'parsers' => [
162 | new ReflectionParser(),
163 | new VisibilityAwarePropertyAccessGuesser(),
164 | ],
165 | 'expectedPropertyCount' => 1,
166 | 'accessType' => [
167 | 'public' => false,
168 | 'hasGetter' => true,
169 | 'hasSetter' => false,
170 | ],
171 | ];
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/tests/ParserTest.php:
--------------------------------------------------------------------------------
1 | parser = new Parser([
34 | new ReflectionParser(),
35 | new PhpDocParser(),
36 | ]);
37 | }
38 |
39 | public function testUnknownClass(): void
40 | {
41 | $this->expectException(ParseException::class);
42 | $this->parser->parse('__invalid__');
43 | }
44 |
45 | public function testSimple(): void
46 | {
47 | $c = new class() {
48 | private $property1;
49 | protected $property2;
50 |
51 | /**
52 | * @var string
53 | */
54 | public $property3;
55 | };
56 |
57 | $classMetadataList = $this->parser->parse(\get_class($c));
58 |
59 | $this->assertCount(1, $classMetadataList, 'Number of class metadata should match');
60 |
61 | $props = $classMetadataList[0]->getPropertyCollections();
62 | $this->assertCount(3, $props, 'Number of class metadata properties should match');
63 |
64 | $this->assertPropertyCollection('property1', 1, $props[0]);
65 | $property1 = $props[0]->getVariations()[0];
66 | $this->assertProperty('property1', false, false, $property1);
67 | $this->assertPropertyType($property1->getType(), PropertyTypeUnknown::class, 'mixed', true);
68 |
69 | $this->assertPropertyCollection('property2', 1, $props[1]);
70 | $property2 = $props[1]->getVariations()[0];
71 | $this->assertProperty('property2', false, false, $property2);
72 | $this->assertPropertyType($property2->getType(), PropertyTypeUnknown::class, 'mixed', true);
73 |
74 | $this->assertPropertyCollection('property3', 1, $props[2]);
75 | $property3 = $props[2]->getVariations()[0];
76 | $this->assertProperty('property3', true, false, $property3);
77 | $this->assertPropertyType($property3->getType(), PropertyTypePrimitive::class, 'string', false);
78 | }
79 |
80 | public function testNested(): void
81 | {
82 | $c = new class() {
83 | /**
84 | * @var Nested
85 | */
86 | private $property;
87 | };
88 |
89 | $classMetadataList = $this->parser->parse(\get_class($c));
90 |
91 | $this->assertCount(2, $classMetadataList, 'Number of class metadata should match');
92 |
93 | // First class
94 |
95 | $props = $classMetadataList[0]->getPropertyCollections();
96 | $this->assertCount(1, $props, 'Number of class metadata properties should match');
97 |
98 | $this->assertPropertyCollection('property', 1, $props[0]);
99 | $property = $props[0]->getVariations()[0];
100 | $this->assertProperty('property', false, false, $property);
101 | $this->assertPropertyType($property->getType(), PropertyTypeClass::class, Nested::class, false);
102 |
103 | // Second class
104 |
105 | $props = $classMetadataList[1]->getPropertyCollections();
106 | $this->assertCount(1, $props, 'Number of class metadata properties should match');
107 |
108 | $this->assertPropertyCollection('nested_property', 1, $props[0]);
109 | $property = $props[0]->getVariations()[0];
110 | $this->assertProperty('nestedProperty', false, false, $property);
111 | $this->assertPropertyType($property->getType(), PropertyTypeUnknown::class, 'mixed', true);
112 | }
113 |
114 | public function testNestedArray(): void
115 | {
116 | $c = new class() {
117 | /**
118 | * @var Nested[]
119 | */
120 | private $property;
121 | };
122 |
123 | $classMetadataList = $this->parser->parse(\get_class($c));
124 |
125 | $this->assertCount(2, $classMetadataList, 'Number of class metadata should match');
126 |
127 | // First class
128 |
129 | $props = $classMetadataList[0]->getPropertyCollections();
130 | $this->assertCount(1, $props, 'Number of class metadata properties should match');
131 |
132 | $this->assertPropertyCollection('property', 1, $props[0]);
133 | $property = $props[0]->getVariations()[0];
134 | $this->assertProperty('property', false, false, $property);
135 | $this->assertPropertyType($property->getType(), PropertyTypeIterable::class, Nested::class.'[]', false);
136 | $this->assertPropertyType($property->getType()->getSubType(), PropertyTypeClass::class, Nested::class, false);
137 |
138 | // Second class
139 |
140 | $props = $classMetadataList[1]->getPropertyCollections();
141 | $this->assertCount(1, $props, 'Number of class metadata properties should match');
142 |
143 | $this->assertPropertyCollection('nested_property', 1, $props[0]);
144 | $property = $props[0]->getVariations()[0];
145 | $this->assertProperty('nestedProperty', false, false, $property);
146 | $this->assertPropertyType($property->getType(), PropertyTypeUnknown::class, 'mixed', true);
147 | }
148 |
149 | private function assertPropertyCollection(string $serializedName, int $variations, PropertyCollection $prop): void
150 | {
151 | $this->assertSame($serializedName, $prop->getSerializedName(), 'Serialized name of property should match');
152 | $this->assertCount($variations, $prop->getVariations(), "Number of variations of property {$serializedName} should match");
153 | }
154 |
155 | private function assertProperty(string $name, bool $public, bool $readOnly, PropertyVariationMetadata $property): void
156 | {
157 | $this->assertSame($name, $property->getName(), 'Name of property should match');
158 | $this->assertSame($public, $property->isPublic(), "Public flag of property {$name} should match");
159 | $this->assertSame($readOnly, $property->isReadOnly(), "Read only flag of property {$name} should match");
160 | }
161 |
162 | private function assertPropertyType(PropertyType $type, string $propertyTypeClass, string $typeString, bool $nullable): void
163 | {
164 | $this->assertInstanceOf($propertyTypeClass, $type);
165 | $this->assertSame($nullable, $type->isNullable());
166 | $this->assertSame($typeString, (string) $type);
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/tests/PropertyReducerTest.php:
--------------------------------------------------------------------------------
1 | assertCount(0, $reduced->getProperties(), 'Number of properties should match');
29 | $this->assertCount(0, $reduced->getPostDeserializeMethods(), 'Number of post deserialize methods should match');
30 | $this->assertCount(0, $reduced->getConstructorParameters(), 'Number of constructor parameters should match');
31 | }
32 |
33 | public function testReduceKeepsPostDeserializeMethods(): void
34 | {
35 | $rawClassMetadata = new RawClassMetadata('Foo');
36 | $rawClassMetadata->addPostDeserializeMethod('method1');
37 | $rawClassMetadata->addPostDeserializeMethod('method2');
38 |
39 | $reduced = PropertyReducer::reduce($rawClassMetadata);
40 |
41 | $this->assertSame(['method1', 'method2'], $reduced->getPostDeserializeMethods());
42 | }
43 |
44 | public function testReduceKeepsConstructorParameters(): void
45 | {
46 | $rawClassMetadata = new RawClassMetadata('Foo');
47 | $rawClassMetadata->addConstructorParameter(new ParameterMetadata('param1', true, null));
48 | $rawClassMetadata->addConstructorParameter(new ParameterMetadata('param2', true, null));
49 |
50 | $reduced = PropertyReducer::reduce($rawClassMetadata);
51 |
52 | $this->assertCount(2, $reduced->getConstructorParameters(), 'Number of constructor parameters should match');
53 | }
54 |
55 | public function testReduceSimpleProperties(): void
56 | {
57 | $rawClassMetadata = new RawClassMetadata('Foo');
58 | $rawClassMetadata->addPropertyVariation('property1', new PropertyVariationMetadata('property1', false, true));
59 | $rawClassMetadata->addPropertyVariation('property2', new PropertyVariationMetadata('property2', false, true));
60 |
61 | $reduced = PropertyReducer::reduce($rawClassMetadata);
62 |
63 | $this->assertProperties(['property1', 'property2'], $reduced->getProperties());
64 | }
65 |
66 | public function testReducePropertiesWithReducers(): void
67 | {
68 | $property1 = new PropertyVariationMetadata('property1', false, true);
69 | $property1->setVersionRange(new VersionRange('1.0', '1.4'));
70 | $property1->setGroups(['group1']);
71 | $property2 = new PropertyVariationMetadata('property2', false, true);
72 |
73 | $rawClassMetadata = new RawClassMetadata('Foo');
74 | $rawClassMetadata->addPropertyVariation('property', $property1);
75 | $rawClassMetadata->addPropertyVariation('property', $property2);
76 |
77 | $reduced = PropertyReducer::reduce($rawClassMetadata, [
78 | new VersionReducer('1.0'),
79 | new GroupReducer(['group1']),
80 | new TakeBestReducer(),
81 | ]);
82 |
83 | $this->assertProperties(['property1'], $reduced->getProperties());
84 | }
85 |
86 | public function testReduceRemovesProperties(): void
87 | {
88 | $property1 = new PropertyVariationMetadata('property1', false, true);
89 | $property2 = new PropertyVariationMetadata('property2', false, true);
90 | $property2->setGroups(['group1']);
91 | $property3 = new PropertyVariationMetadata('property3', false, true);
92 | $property3->setGroups(['group1', 'group2']);
93 |
94 | $rawClassMetadata = new RawClassMetadata('Foo');
95 | $rawClassMetadata->addPropertyVariation('property1', $property1);
96 | $rawClassMetadata->addPropertyVariation('property2', $property2);
97 | $rawClassMetadata->addPropertyVariation('property3', $property3);
98 |
99 | $reduced = PropertyReducer::reduce($rawClassMetadata, [
100 | new VersionReducer('1.0'),
101 | new GroupReducer(['group1']),
102 | new TakeBestReducer(),
103 | ]);
104 |
105 | $this->assertProperties(['property2', 'property3'], $reduced->getProperties());
106 | }
107 |
108 | /**
109 | * @param string[] $propertyNames
110 | * @param PropertyVariationMetadata[] $properties
111 | */
112 | private function assertProperties(array $propertyNames, iterable $properties): void
113 | {
114 | $names = [];
115 | foreach ($properties as $property) {
116 | $names[] = $property->getName();
117 | }
118 |
119 | $this->assertSame($propertyNames, $names, 'Properties should match');
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/tests/RecursionContextTest.php:
--------------------------------------------------------------------------------
1 | assertStringContainsString('Root', $s);
22 | $this->assertStringNotContainsString('property1', $s);
23 | $this->assertStringNotContainsString('property2', $s);
24 | }
25 |
26 | public function testPush(): void
27 | {
28 | $context = new RecursionContext('Root');
29 | $context = $context->push(new PropertyMetadata('property1', 'property1'));
30 | $context = $context->push(new PropertyMetadata('property2', 'property2'));
31 |
32 | $s = (string) $context;
33 | $this->assertStringContainsString('Root', $s);
34 | $this->assertStringContainsString('property1', $s);
35 | $this->assertStringContainsString('property2', $s);
36 | }
37 |
38 | public function testMatchesEmpty(): void
39 | {
40 | $context = new RecursionContext('Root');
41 |
42 | $this->assertFalse($context->matches([]));
43 | $this->assertFalse($context->matches(['foo', 'bar', 'baz']));
44 | }
45 |
46 | public function testMatches(): void
47 | {
48 | $context = new RecursionContext('Root');
49 | $context = $context->push(new PropertyMetadata('property1', 'property1'));
50 | $context = $context->push(new PropertyMetadata('property2', 'property2'));
51 |
52 | $this->assertFalse($context->matches(['Root', 'property2']));
53 | $this->assertFalse($context->matches(['property2', 'property1']));
54 | $this->assertTrue($context->matches(['Root', 'property1']));
55 | $this->assertTrue($context->matches(['Root', 'property1', 'property2']));
56 | $this->assertTrue($context->matches(['property1', 'property2']));
57 | $this->assertTrue($context->matches(['property2']));
58 | }
59 |
60 | public function testMatchesWildcard(): void
61 | {
62 | $context = new RecursionContext('Root');
63 | $context = $context->push(new PropertyMetadata('property1', 'property1'));
64 | $context = $context->push(new PropertyMetadata('property2', 'property2'));
65 |
66 | $this->assertFalse($context->matches(['Root', '*', 'property1']));
67 | $this->assertFalse($context->matches(['*', 'property1']));
68 | $this->assertTrue($context->matches(['Root', '*']));
69 | $this->assertTrue($context->matches(['Root', '*', 'property2']));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/Reducer/GroupReducerTest.php:
--------------------------------------------------------------------------------
1 | setGroups(['group1', 'group2']);
21 | $property3 = new PropertyVariationMetadata('property3', false, true);
22 | $property3->setGroups(['group3']);
23 |
24 | $properties = [
25 | $property1,
26 | $property2,
27 | $property3,
28 | ];
29 |
30 | $reducedProperties = (new GroupReducer(['group1']))->reduce('property', $properties);
31 | $this->assertProperties(['property2'], $reducedProperties);
32 |
33 | $reducedProperties = (new GroupReducer(['group2']))->reduce('property', $properties);
34 | $this->assertProperties(['property2'], $reducedProperties);
35 |
36 | $reducedProperties = (new GroupReducer(['group3']))->reduce('property', $properties);
37 | $this->assertProperties(['property3'], $reducedProperties);
38 |
39 | $reducedProperties = (new GroupReducer(['group1', 'group2', 'group3']))->reduce('property', $properties);
40 | $this->assertProperties(['property2', 'property3'], $reducedProperties);
41 |
42 | $reducedProperties = (new GroupReducer([]))->reduce('property', $properties);
43 | $this->assertProperties(['property1', 'property2', 'property3'], $reducedProperties);
44 | }
45 |
46 | /**
47 | * @param string[] $propertyNames
48 | * @param PropertyVariationMetadata[] $properties
49 | */
50 | private function assertProperties(array $propertyNames, iterable $properties): void
51 | {
52 | $names = [];
53 | foreach ($properties as $property) {
54 | $names[] = $property->getName();
55 | }
56 |
57 | $this->assertSame($propertyNames, $names, 'Properties should match');
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Reducer/TakeBestReducerTest.php:
--------------------------------------------------------------------------------
1 | reduce('property', $properties);
24 | $this->assertProperties(['property', 'other'], $reducedProperties);
25 | }
26 |
27 | public function testReduceWithDifferentNames(): void
28 | {
29 | $properties = [
30 | new PropertyVariationMetadata('other', false, true),
31 | new PropertyVariationMetadata('other2', false, true),
32 | ];
33 |
34 | $reducedProperties = (new TakeBestReducer())->reduce('property', $properties);
35 | $this->assertProperties(['other', 'other2'], $reducedProperties);
36 | }
37 |
38 | /**
39 | * @param string[] $propertyNames
40 | * @param PropertyVariationMetadata[] $properties
41 | */
42 | private function assertProperties(array $propertyNames, iterable $properties): void
43 | {
44 | $names = [];
45 | foreach ($properties as $property) {
46 | $names[] = $property->getName();
47 | }
48 |
49 | $this->assertSame($propertyNames, $names, 'Properties should match');
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/Reducer/VersionReducerTest.php:
--------------------------------------------------------------------------------
1 | setVersionRange(new VersionRange(null, '1.4'));
21 | $property2 = new PropertyVariationMetadata('property2', false, true);
22 | $property2->setVersionRange(new VersionRange('2.0', '2.2'));
23 | $property3 = new PropertyVariationMetadata('property3', false, true);
24 | $property3->setVersionRange(new VersionRange('3.0', null));
25 |
26 | $properties = [
27 | $property1,
28 | $property2,
29 | $property3,
30 | ];
31 |
32 | $reducedProperties = (new VersionReducer('0.3'))->reduce('property', $properties);
33 | $this->assertProperties(['property1'], $reducedProperties);
34 |
35 | $reducedProperties = (new VersionReducer('1.0'))->reduce('property', $properties);
36 | $this->assertProperties(['property1'], $reducedProperties);
37 |
38 | $reducedProperties = (new VersionReducer('2.0'))->reduce('property', $properties);
39 | $this->assertProperties(['property2'], $reducedProperties);
40 |
41 | $reducedProperties = (new VersionReducer('3.0'))->reduce('property', $properties);
42 | $this->assertProperties(['property3'], $reducedProperties);
43 |
44 | $reducedProperties = (new VersionReducer('4.0'))->reduce('property', $properties);
45 | $this->assertProperties(['property3'], $reducedProperties);
46 |
47 | $reducedProperties = (new VersionReducer('1.9'))->reduce('property', $properties);
48 | $this->assertProperties([], $reducedProperties);
49 | }
50 |
51 | /**
52 | * @param string[] $propertyNames
53 | * @param PropertyVariationMetadata[] $properties
54 | */
55 | private function assertProperties(array $propertyNames, iterable $properties): void
56 | {
57 | $names = [];
58 | foreach ($properties as $property) {
59 | $names[] = $property->getName();
60 | }
61 |
62 | $this->assertSame($propertyNames, $names, 'Properties should match');
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/tests/TypeParser/JMSTypeParserTest.php:
--------------------------------------------------------------------------------
1 | parser = new JMSTypeParser();
25 | }
26 |
27 | public function provideTypeCases(): iterable
28 | {
29 | yield [
30 | '',
31 | 'mixed',
32 | true,
33 | ];
34 |
35 | yield [
36 | 'array',
37 | 'array',
38 | true,
39 | ];
40 |
41 | yield [
42 | 'string',
43 | 'string|null',
44 | ];
45 |
46 | yield [
47 | 'boolean',
48 | 'bool|null',
49 | ];
50 |
51 | yield [
52 | 'integer',
53 | 'int|null',
54 | ];
55 |
56 | yield [
57 | 'double',
58 | 'float|null',
59 | ];
60 |
61 | yield [
62 | 'stdClass',
63 | 'stdClass|null',
64 | ];
65 |
66 | yield [
67 | 'array',
68 | 'string[]|null',
69 | ];
70 |
71 | yield [
72 | 'array>>',
73 | 'bool[][][]|null',
74 | ];
75 |
76 | yield [
77 | 'array',
78 | 'int[]|null',
79 | ];
80 |
81 | yield [
82 | 'array',
83 | 'int[string]|null',
84 | ];
85 |
86 | yield [
87 | 'array>>',
88 | 'bool[string][string][string]|null',
89 | ];
90 |
91 | yield [
92 | 'array>>',
93 | 'bool[string][][string]|null',
94 | ];
95 | }
96 |
97 | /**
98 | * @dataProvider provideTypeCases
99 | */
100 | public function testType(string $rawType, string $expectedType, bool $expectedNullable = null): void
101 | {
102 | $type = $this->parser->parse($rawType);
103 |
104 | $this->assertSame($expectedType, (string) $type, 'Type should match');
105 | if (null !== $expectedNullable) {
106 | $this->assertSame($expectedNullable, $type->isNullable(), 'Nullable flag should match');
107 | }
108 | }
109 |
110 | public function provideDateTimeTypeCases(): iterable
111 | {
112 | yield [
113 | 'DateTime',
114 | 'DateTime|null',
115 | null,
116 | null,
117 | null,
118 | ];
119 |
120 | yield [
121 | 'DateTime<\'Y-m-d H:i:s\'>',
122 | 'DateTime|null',
123 | 'Y-m-d H:i:s',
124 | null,
125 | 'Y-m-d H:i:s',
126 | ];
127 |
128 | yield [
129 | 'DateTime<\'\', \'Europe/Zurich\'>',
130 | 'DateTime|null',
131 | null,
132 | 'Europe/Zurich',
133 | null,
134 | ];
135 |
136 | yield [
137 | 'DateTime<\'\', \'\', \'Y-m-d\'>',
138 | 'DateTime|null',
139 | null,
140 | null,
141 | 'Y-m-d',
142 | ];
143 |
144 | yield [
145 | 'DateTime<\'Y-m-d H:i:s\', \'Europe/Zurich\', \'Y-m-d\'>',
146 | 'DateTime|null',
147 | 'Y-m-d H:i:s',
148 | 'Europe/Zurich',
149 | 'Y-m-d',
150 | ];
151 |
152 | yield [
153 | 'DateTimeImmutable',
154 | 'DateTimeImmutable|null',
155 | null,
156 | null,
157 | null,
158 | ];
159 |
160 | yield [
161 | 'DateTimeImmutable<\'Y-m-d H:i:s\'>',
162 | 'DateTimeImmutable|null',
163 | 'Y-m-d H:i:s',
164 | null,
165 | 'Y-m-d H:i:s',
166 | ];
167 |
168 | yield [
169 | 'DateTimeImmutable<\'\', \'Europe/Zurich\'>',
170 | 'DateTimeImmutable|null',
171 | null,
172 | 'Europe/Zurich',
173 | null,
174 | ];
175 |
176 | yield [
177 | 'DateTimeImmutable<\'\', \'\', \'Y-m-d\'>',
178 | 'DateTimeImmutable|null',
179 | null,
180 | null,
181 | 'Y-m-d',
182 | ];
183 |
184 | yield [
185 | 'DateTimeImmutable<\'Y-m-d H:i:s\', \'Europe/Zurich\', \'Y-m-d\'>',
186 | 'DateTimeImmutable|null',
187 | 'Y-m-d H:i:s',
188 | 'Europe/Zurich',
189 | 'Y-m-d',
190 | ];
191 | }
192 |
193 | /**
194 | * @dataProvider provideDateTimeTypeCases
195 | */
196 | public function testDateTimeType(string $rawType, string $expectedType, ?string $expectedFormat, ?string $expectedZone, ?string $expectedDeserializeFormat): void
197 | {
198 | /** @var PropertyTypeDateTime $type */
199 | $type = $this->parser->parse($rawType);
200 | $this->assertInstanceOf(PropertyTypeDateTime::class, $type);
201 | $this->assertSame($expectedType, (string) $type, 'Type should match');
202 | $this->assertSame($expectedFormat, $type->getFormat(), 'Date time format should match');
203 | $this->assertSame($expectedZone, $type->getZone(), 'Date time zone should match');
204 | $this->assertSame($expectedDeserializeFormat, $type->getDeserializeFormat(), 'Date time deserialize format should match');
205 | $this->assertSame($expectedDeserializeFormat ? [$expectedDeserializeFormat] : null, $type->getDeserializeFormats(), 'Date time deserialize format should match');
206 | }
207 |
208 | public function testInvalidTypeWithParameters(): void
209 | {
210 | $this->expectException(InvalidTypeException::class);
211 | $this->parser->parse('stdClass');
212 | }
213 |
214 | public function testArrayWithTooManyParameters(): void
215 | {
216 | $this->expectException(InvalidTypeException::class);
217 | $this->parser->parse('array');
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/tests/TypeParser/PhpTypeParserTest.php:
--------------------------------------------------------------------------------
1 | parser = new PhpTypeParser();
29 | }
30 |
31 | public function providePropertyTypeCases(): iterable
32 | {
33 | yield [
34 | '',
35 | 'mixed',
36 | true,
37 | ];
38 |
39 | yield [
40 | 'mixed',
41 | 'mixed',
42 | true,
43 | ];
44 |
45 | yield [
46 | 'object',
47 | 'mixed',
48 | false,
49 | ];
50 |
51 | yield [
52 | 'array',
53 | 'array',
54 | false,
55 | ];
56 |
57 | yield [
58 | 'string',
59 | 'string',
60 | ];
61 |
62 | yield [
63 | 'boolean',
64 | 'bool',
65 | ];
66 |
67 | yield [
68 | 'integer',
69 | 'int',
70 | ];
71 |
72 | yield [
73 | 'double',
74 | 'float',
75 | ];
76 |
77 | yield [
78 | 'int|null',
79 | 'int|null',
80 | ];
81 |
82 | yield [
83 | '\stdClass|null',
84 | 'stdClass|null',
85 | ];
86 |
87 | yield [
88 | '\DateTime',
89 | 'DateTime',
90 | ];
91 |
92 | yield [
93 | '\DateTimeImmutable',
94 | 'DateTimeImmutable',
95 | ];
96 |
97 | yield [
98 | 'string[]',
99 | 'string[]',
100 | ];
101 |
102 | yield [
103 | 'string[][][]',
104 | 'string[][][]',
105 | ];
106 |
107 | yield [
108 | 'string[string]',
109 | 'string[string]',
110 | ];
111 |
112 | yield [
113 | 'string[string][string][string]',
114 | 'string[string][string][string]',
115 | ];
116 |
117 | yield [
118 | 'string[][string][]',
119 | 'string[][string][]',
120 | ];
121 |
122 | yield [
123 | '\stdClass[]|null',
124 | 'stdClass[]|null',
125 | ];
126 |
127 | yield [
128 | 'string[]|\Doctrine\Common\Collections\Collection|null',
129 | 'string[]|\Doctrine\Common\Collections\Collection|null',
130 | ];
131 |
132 | yield [
133 | '\stdClass[][string]',
134 | 'stdClass[][string]',
135 | ];
136 | }
137 |
138 | public function providePropertyTypeArrayIsCollectionCases(): iterable
139 | {
140 | yield [
141 | 'string[]|\Doctrine\Common\Collections\Collection',
142 | ];
143 |
144 | yield [
145 | 'string[]|\Doctrine\Common\Collections\ArrayCollection',
146 | ];
147 | }
148 |
149 | /**
150 | * @dataProvider providePropertyTypeCases
151 | */
152 | public function testPropertyType(string $rawType, string $expectedType, bool $expectedNullable = null): void
153 | {
154 | $type = $this->parser->parseAnnotationType($rawType, new \ReflectionClass($this));
155 |
156 | $this->assertSame($expectedType, (string) $type, 'Type should match');
157 | if (null !== $expectedNullable) {
158 | $this->assertSame($expectedNullable, $type->isNullable(), 'Nullable flag should match');
159 | }
160 | }
161 |
162 | /**
163 | * @dataProvider providePropertyTypeArrayIsCollectionCases
164 | */
165 | public function testPropertyTypeArrayIsCollection(string $rawType): void
166 | {
167 | $type = $this->parser->parseAnnotationType($rawType, new \ReflectionClass($this));
168 | self::assertInstanceOf(PropertyTypeIterable::class, $type);
169 | self::assertTrue($type->isTraversable());
170 | }
171 |
172 | public function testMultiType(): void
173 | {
174 | $this->expectException(InvalidTypeException::class);
175 | $this->parser->parseAnnotationType('string|int', new \ReflectionClass($this));
176 | }
177 |
178 | public function testResourceType(): void
179 | {
180 | $this->expectException(InvalidTypeException::class);
181 | $this->parser->parseAnnotationType('resource', new \ReflectionClass($this));
182 | }
183 |
184 | public function provideNamespaceResolutionCases(): iterable
185 | {
186 | yield [
187 | 'ReflectionAbstractModel',
188 | ReflectionAbstractModel::class,
189 | ];
190 |
191 | yield [
192 | 'ReflectionBaseModel',
193 | RecursionContextTest::class,
194 | ];
195 |
196 | yield [
197 | 'Nested',
198 | BaseModel::class,
199 | ];
200 |
201 | yield [
202 | 'Nested[string][]',
203 | BaseModel::class.'[string][]',
204 | ];
205 |
206 | yield [
207 | 'Nested[]|Collection',
208 | BaseModel::class.'[]|\Doctrine\Common\Collections\Collection<'.BaseModel::class.'>',
209 | ];
210 | }
211 |
212 | /**
213 | * @dataProvider provideNamespaceResolutionCases
214 | */
215 | public function testNamespaceResolution(string $rawType, string $expectedType): void
216 | {
217 | $type = $this->parser->parseAnnotationType($rawType, new \ReflectionClass(WithImports::class));
218 |
219 | $this->assertSame($expectedType, (string) $type, 'Type should match');
220 | }
221 |
222 | public function provideReflectionTypeCases(): iterable
223 | {
224 | $c = new class() {
225 | private function method1(): string
226 | {
227 | return '1';
228 | }
229 |
230 | private function method2(): ?int
231 | {
232 | return 1;
233 | }
234 |
235 | private function method3(): array
236 | {
237 | return [1];
238 | }
239 | };
240 | $reflClass = new \ReflectionClass(\get_class($c));
241 |
242 | yield [
243 | $reflClass->getMethod('method1')->getReturnType(),
244 | 'string',
245 | ];
246 |
247 | yield [
248 | $reflClass->getMethod('method2')->getReturnType(),
249 | 'int|null',
250 | ];
251 |
252 | yield [
253 | $reflClass->getMethod('method3')->getReturnType(),
254 | 'array',
255 | false,
256 | ];
257 | }
258 |
259 | /**
260 | * @dataProvider provideReflectionTypeCases
261 | */
262 | public function testReflectionType(\ReflectionType $reflType, string $expectedType, bool $expectedNullable = null): void
263 | {
264 | $type = $this->parser->parseReflectionType($reflType);
265 |
266 | $this->assertSame($expectedType, (string) $type, 'Type should match');
267 | if (null !== $expectedNullable) {
268 | $this->assertSame($expectedNullable, $type->isNullable(), 'Nullable flag should match');
269 | }
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | add('JMS\Serializer\Tests', __DIR__);
11 | })();
12 |
--------------------------------------------------------------------------------