├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── composer.json
├── phpcs.xml
├── phpstan.neon
├── phpunit.xml
├── src
├── Component
│ ├── Architecture.php
│ └── Component.php
├── Composer
│ ├── CachedComposerFileParserFactory.php
│ ├── ComposerFileParser.php
│ ├── ComposerFileParserCacheDecorator.php
│ ├── ComposerFileParserFactory.php
│ ├── ComposerFileParserFactoryInterface.php
│ └── ComposerFileParserInterface.php
├── Exception
│ ├── CodeAnalysisException.php
│ ├── ComponentNotDefinedException.php
│ └── Exception.php
├── Parser
│ ├── Parser.php
│ ├── ParserException.php
│ └── Visitor
│ │ ├── DocBlockTypeAnnotations.php
│ │ ├── ExtractDeclaredNamespace.php
│ │ ├── FullyQualifiedReference.php
│ │ ├── NamespaceCollectingVisitor.php
│ │ └── UseStatement.php
├── PhpArch.php
├── Utility
│ ├── ArrayUtility.php
│ ├── NamespaceComperator.php
│ └── NamespaceComperatorCollection.php
└── Validation
│ ├── AbstractValidationCollection.php
│ ├── AllowInterfaces.php
│ ├── ExplicitlyAllowDependency.php
│ ├── ForbiddenDependency.php
│ ├── MustBeSelfContained.php
│ ├── MustOnlyDependOn.php
│ ├── MustOnlyDependOnComposerDependencies.php
│ ├── MustOnlyHaveAutoloadableDependencies.php
│ ├── ValidationCollection.php
│ └── Validator.php
└── tests
├── ArchitectureTest.php
├── Component
├── ArchitectureTest.php
├── ComponentTest.php
└── Example
│ ├── Allowed
│ └── AllowedDependency.php
│ ├── Forbidden
│ └── ForbiddenDependency.php
│ ├── OutsideDependency.php
│ └── Test
│ ├── InsideDependency.php
│ └── TestClass.php
├── Composer
├── ComposerFileParserTest.php
└── Mock
│ ├── .gitignore
│ ├── composer.json
│ └── composer.lock
├── Parser
└── Visitor
│ ├── DocBlockTypeAnnotationsTest.php
│ ├── Example
│ ├── DocBlock
│ │ ├── AnonymousClassDocBlockArgument.php
│ │ ├── DocBlockArgument.php
│ │ ├── DocBlockArrayItem.php
│ │ ├── DocBlockGenericTypeArrayLong.php
│ │ ├── DocBlockGenericTypeArrayShort.php
│ │ ├── DocBlockReturn.php
│ │ ├── DocBlockTypedTemplate.php
│ │ ├── DocBlockUnionGenericChildA.php
│ │ ├── DocBlockUnionGenericChildB.php
│ │ ├── DocBlockUnionGenericChildC.php
│ │ ├── DocBlockUnionGenericChildD.php
│ │ ├── DocBlockUnionGenericWrapperA.php
│ │ ├── DocBlockUnionGenericWrapperB.php
│ │ ├── DocBlockUnionTypeA.php
│ │ ├── DocBlockUnionTypeB.php
│ │ ├── GenericPseudoType.php
│ │ ├── ImportedAnonymousClassDocBlockArgument.php
│ │ ├── ImportedDocBlockArgument.php
│ │ ├── ImportedDocBlockReturn.php
│ │ ├── ImportedGenericArgument.php
│ │ └── ImportedGenericClassArgument.php
│ ├── InstanceCreation
│ │ ├── ImportedInstanceCreation.php
│ │ └── InstanceCreation.php
│ ├── ParentClass.php
│ ├── SomeInterface.php
│ ├── StaticMethodCall
│ │ ├── ImportedStaticMethodCall.php
│ │ └── StaticMethodCall.php
│ ├── TestClass.php
│ ├── Traits
│ │ ├── ImporetdTrait.php
│ │ └── UsedTrait.php
│ └── TypeAnnotation
│ │ ├── ArgumentAnnotation.php
│ │ ├── ImportedArgumentAnnotation.php
│ │ ├── ImportedReturnTypeAnnotation.php
│ │ └── ReturnTypeAnnotation.php
│ ├── ExtractDeclaredNamespaceTest.php
│ ├── FullyQualifiedReferencedTest.php
│ └── UseStatementTest.php
├── TestCase.php
├── Utility
├── NamespaceComperatorCollectionTest.php
└── NamespaceComperatorTest.php
└── Validation
├── ForbiddenDependencyTest.php
├── Mock
├── ExistingClass.php
├── ExistingInterface.php
├── ExistingTrait.php
├── TestClass.php
└── TestValidator.php
├── MustBeSelfContainedTest.php
├── MustOnlyDependOnTest.php
├── MustOnlyHaveAutoloadableDependenciesTest.php
└── ValidationCollectionTest.php
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | sniff:
7 | name: 🕵️ PHP ${{ matrix.php-version }} Code Sniffer
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | php-version: ["7.4", "8.0", "8.1"]
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: 🛒 Checkout
15 | uses: actions/checkout@v2
16 | - name: ⚙️ Install composer
17 | uses: php-actions/composer@v5
18 | with:
19 | php_version: ${{ matrix.php-version }}
20 | - name: 🕵️ Run Code Sniffer
21 | run: ./vendor/bin/phpcs -vvv
22 |
23 | analyze:
24 | name: 🔎 PHP ${{ matrix.php-version }} Static Analysis
25 | strategy:
26 | fail-fast: false
27 | matrix:
28 | php-version: ["7.4", "8.0", "8.1"]
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: 🛒 Checkout
32 | uses: actions/checkout@v2
33 | - name: ⚙️ Install composer
34 | uses: php-actions/composer@v5
35 | with:
36 | php_version: ${{ matrix.php-version }}
37 | - name: 🔎 Perform Static Analysis
38 | run: ./vendor/bin/phpstan -vvv
39 |
40 | test:
41 | name: 🧪 PHP ${{ matrix.php-version }} Unit Testing
42 | strategy:
43 | fail-fast: false
44 | matrix:
45 | php-version: ["7.4", "8.0", "8.1"]
46 | runs-on: ubuntu-latest
47 | steps:
48 | - name: 🛒 Checkout
49 | uses: actions/checkout@v2
50 | - name: ⚙️ Install composer
51 | uses: php-actions/composer@v5
52 | with:
53 | php_version: ${{ matrix.php-version }}
54 | - name: 🧪 Run Tests
55 | run: ./vendor/bin/phpunit -vvv
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | .idea/
3 | composer.lock
4 | coverage/
5 | .phpunit.result.cache
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - '7.4'
5 | - '8.0'
6 | env:
7 | - COMPOSER_LOWEST=0
8 | - COMPOSER_LOWEST=1
9 |
10 | matrix:
11 | exclude:
12 | - php: '7.3'
13 | env: COMPOSER_LOWEST=1
14 |
15 | install:
16 | - '[ $COMPOSER_LOWEST == 1 ] && composer update --prefer-lowest || composer install'
17 |
18 | script:
19 | - ./vendor/bin/phpcs
20 | - ./vendor/bin/phpstan
21 | - ./vendor/bin/phpunit
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [3.1.1] - 2022-04-14
8 | ### Added
9 | * Broadened the requirement for `symfony/finder` to include version 6.x to be compatible with symfony 6.x projects.
10 |
11 | ## [3.1.0] - 2022-02-28
12 | ### Added
13 | - `Architecture` now uses a `ComposerFileParserFactory` to create a file parser. `CachedComposerFileParserFactory` can be used for caching of results in order to prevent parsing the file multiple times.
14 |
15 |
16 | ## [3.0.0] - 2021-01-31
17 | ### Added
18 | - PHP8 is now supported
19 |
20 | ### Removed
21 | - PHP 7.3 is no longer supported
22 | - The minimum compatible version of `symfony/finder` was lifted from 3.* to 4.* making this
23 | update incompatible with symfony 3.* projets.
24 |
25 | ## [2.0.1] - 2021-01-28
26 | ### Fixed
27 | - Fixed a typo in the error message of `mustOnlyDependOn`
28 |
29 | ## [2.0.0] - 2020-10-14
30 | ### Removed
31 | - Support for PHP 7.2 was dropped
32 |
33 | ### Changed
34 | * Dependencies were updated
35 | * `phpunit/phpunit` to 9.4+
36 | * `thecodingmachine/safe` to 1.3+
37 | * and more...
38 | * Added type hints to more methods
39 |
40 | ## [1.2.0] - 2020-07-27
41 | ### Added
42 | - Types used inside of generics are now also tracked correctly.
43 |
44 | ## [1.1.2] - 2020-03-02
45 | ### Added
46 | - Support for `symfony/finder` 5.x. This means phparch will install in symfony 4 & 5 environments.
47 |
48 | ## [1.1.1] - 2019-05-19
49 | ### Fixed
50 | - Architectures are now not validated to only have autoloadable dependencies anymore because
51 | this validator disregards namespace imports.
52 |
53 | ## [1.1.0] - 2019-05-19
54 | ### Added
55 | - A new `MustOnlyHaveAutoloadableDependencies` validator has been added in order to prevent accidental dependencies
56 | to unrelated packages that just happen to be used in the same system often.
57 | - All components in architectures are now being checked against this new validator.
58 | - A new `MustOnlyDependOnComposerDependencies` validator has been added in order to prevent accidentally using
59 | namespaces that are not also declared in `composer.json`.
60 | - A new `ExplicitlyAllowDependency` validator allows explicitly allowing dependencies from one component to another.
61 | - Architectures now have a bunch of new helper methods
62 | - `mustOnlyDependOnComposerDependencies` adds `MustOnlyDependOnComposerDependencies` validator.
63 | - `addComposerBasedComponent` initializes a full component from the given `composer.json` file and
64 | adds a `MustOnlyDependOnComposerDependencies` validator
65 | - `isAllowedToDependOn` allows dependencies from one component to another one.
66 | - `disallowInterdependence` makes it easy to disallow dependence between many different components.
67 | - `mustNotDependOnAnyOtherComponent` makes it easy to declare core components that should not depend on
68 | anything else that is architecturally significant.
69 |
70 | ### Removed
71 | - Support for PHP 7.1 was dropped
72 |
73 | ## [1.0.0] - 2019-02-18
74 | ### Added
75 | - Allowing dependencies to Interfaces only is now possible
76 | - Using the `AllowInterfaces` Validation wrapper
77 | - Using the `mustNotDependOn` method on a component
78 | - Using the `mustNotDirectlyDependOn` method on an architecture
79 | - Bulk declaration of components using the `Architecture->components` method is now possible
80 |
81 | ### Fixed
82 | - Dependencies of anonymous / inner classes are now correctly tracked
83 |
84 |
85 | ## [0.3.1] - 2018-12-12
86 | ### Fixed
87 | - Not loading tests into production autoloader anymore
88 | - Now ignoring references to non-existent classes
89 | - Fixed Validators not being able to be serialized correctly for error output
90 |
91 |
92 | ## [0.3.0] - 2018-12-01
93 | ### Added
94 | - Now correctly identifies the following types of dependencies (though in most cases they have already been tracked through `use` statements):
95 | - DocBlock comments
96 | - Static method calls
97 | - Argument type annotations
98 | - Return type annotations
99 |
100 | ### Fixed
101 | - Namespace comparisons against higher up namespaces no longer match
102 |
103 |
104 | ## [0.2.0] - 2018-11-30
105 | ### Added
106 | - New `assertHasNoErrors` method was added to make checking for architecture violations in PHPUnit easier.
107 |
108 | ### Changed
109 | - Switched from using php-dependency-analysis to manually using `nikic/php-parser` - phpda uses outdated dependencies
110 | that are hard to install on many up-to-date systems (such as a current laravel installation).
111 |
112 |
113 | ## [0.1.0] - 2018-11-29
114 | ### Added
115 | - Initial release. Literally everything was added.
116 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution Guidelines
2 |
3 | Contributions are always welcome - just open an issue or PR.
4 | Please be aware that even though it may not seem like it there are always other humans on the other end of a conversation.
5 |
6 | **Please treat others with respect and kindness.**
7 |
8 | ## Git commit messages
9 |
10 | Commits made to this repository should follow the following guidelines look like the following:
11 |
12 | ```
13 | !!! CLEANUP | Short message in present tense
14 | ```
15 |
16 | - `!!! ` (optional): Three exclamation marks should be added in front of the commit message if it contains breaking changes.
17 | - `CLEANUP |`: A commit category and separator must follow valid categories are the following:
18 | - `FEATURE`: New functionality was added.
19 | - `BUGFIX`: Broken functionality was restored.
20 | - `CLEANUP`: No functional changes, only non-functionals. Use this if you reformat code or edit comments.
21 | - `TASK`: For everything that is not one of the above.
22 | - `RELEASE`: Only for release versions.
23 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2018 Johannes Hertenstein
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHPArch is no longer maintained
2 |
3 | This project is no longer maintained. When I started development, similar tools did not yet exist
4 | or, in the case of deptrac, did not cover my use-case. By now, better tools exist and I am happy
5 | to point users towards those.
6 |
7 | * [PHPAt](https://github.com/carlosas/phpat) is the spiritual sucessor to PHPArch and even contains some of it's code
8 | * [Deptrac](https://github.com/qossmic/deptrac) takes more of a graph-based approach to the problem
9 |
10 | Both serve as good replacements if you have previously used PHPArch.
11 |
12 | ---
13 |
14 | # PHPArch [](https://travis-ci.org/j6s/phparch)
15 |
16 | - [What is this?](#what-is-this)
17 | - [Installation](#installation)
18 | - [Simple Namespace validation](#simple-namespace-validation)
19 | - [Available Validators](#available-validators)
20 | - [Defining an architecture](#defining-an-architecture)
21 | - [Syntactic sugar: Bulk definition of components](#syntactic-sugar-bulk-definition-of-components)
22 | - [Syntactic sugar: Chaining multiple dependency rules](#syntactic-sugar-chaining-multiple-dependency-rules)
23 | - [Examples](#examples)
24 | ## What is this?
25 |
26 | PHPArch is a work in progress architectural testing library for PHP projects.
27 | It is inspired by [archlint (C#)](https://gitlab.com/iternity/archlint.cs)
28 | and [archunit (java)](https://github.com/TNG/ArchUnit).
29 |
30 | It can be used to help enforce architectural boundaries in an application in order
31 | to prevent the architecture from rotting over time by introducing dependencies across
32 | previously well defined architectural boundaries.
33 |
34 | ## Installation
35 |
36 | You can install PHPArch using composer.
37 | If you don't know what composer is then you probably don't need a library for architectural testing.
38 |
39 | ```bash
40 | $ composer require j6s/phparch
41 | ```
42 |
43 | ## Simple Namespace validation
44 |
45 | The most simple type of check PHPArch can help you with are simple namespace based checks:
46 | Setup rules for which namespace is allowed or forbidden to depend on which other namespace.
47 |
48 | ```php
49 | public function testSimpleNamespaces()
50 | {
51 | (new PhpArch())
52 | ->fromDirectory(__DIR__ . '/../../app')
53 | ->validate(new ForbiddenDependency('Lib\\', 'App\\'))
54 | ->validate(new MustBeSelfContained('App\\Utility'))
55 | ->validate(new MustOnlyDependOn('App\\Mailing', 'PHPMailer\\PHPMailer'))
56 | ->assertHasNoErrors();
57 | }
58 | ```
59 |
60 | ### Available Validators
61 | Currently the following validators are available:
62 | - `ForbiddenDependency` Lets you declare that one namespace is not allowed to depend on another namespace.
63 | - `MustBeSelfContained` Lets you declare that a namespace must be self-contained meaning that it may not have
64 | any external dependencies.
65 | - `MustOnlyDependOn` Lets you declare that one namespace must only depend on another namespace.
66 | - `MustOnlyHaveAutoloadableDependencies` checks if all dependencies are autoloadable in the current environment.
67 | This can be helpful if two packages should not have any dependencies on each other but they still sneak in because
68 | the packages are often used together.
69 | - `AllowInterfaces` is a wrapper for validators that allows dependencies if they are to interfaces.
70 | - `MustOnlyDependOnComposerDependencies` checks if all dependencies in a given namespace are declared in the given
71 | `composer.json` file. This is useful to prevent accidental dependencies if one repository contains multiple packages.
72 | - `ExplicitlyAllowDependency` is a wrapper for validators that allows a specific dependency.
73 |
74 | Most architectural boundaries can be described with these rules.
75 |
76 | ## Defining an architecture
77 |
78 | PHPArch also contains a fluent API that allows you to define a component based architecture which is then validated.
79 | The API is based on components which are identified by one or more namespaces instead of Layers or 'Onion Peels' because
80 | it is the simplest way to communicate any architecture - no matter what the implementation details of it are.
81 |
82 | ```php
83 | public function testArchitecture()
84 | {
85 | $architecture = (new Architecture())
86 | ->component('Components')->identifiedByNamespace('J6s\\PhpArch\\Component')
87 | ->mustNotDependOn('Validation')->identifiedByNamespace('J6s\\PhpArch\\Validation');
88 |
89 | (new PhpArch())
90 | ->fromDirectory(__DIR__ . '/../../app')
91 | ->validate($architecture)
92 | ->assertHasNoErrors();
93 | }
94 | ```
95 |
96 | Most of defining an architecture is only syntactic sugar over the namespace validators above.
97 | The following methods allow you to add assertions to your component structure:
98 |
99 | - `mustNotDependOn`
100 | - `mustNotDependDirectlyOn`
101 | - `mustNotBeDependedOnBy`
102 | - `mustOnlyDependOn`
103 | - `mustNotDependOnAnyOtherComponent`
104 | - `mustOnlyDependOnComposerDependencies`
105 | - `dissallowInterdependence`
106 | - `isAllowedToDependOn`
107 |
108 | ### Syntactic sugar: Bulk definition of components
109 |
110 | While the speaking Api for defining an architecture is great it can get convoluted and
111 | hard to read if you have a lot of components. The `components` method can be used to define
112 | components using a simple associative array where the key is the component name and the
113 | value is the namespaces that define the component. This way definitions of components and
114 | setting up dependency rules can be split into 2 steps for better readability.
115 |
116 | ```php
117 | // This
118 | $architecture->components([
119 | 'Foo' => 'Vendor\\Foo',
120 | 'Bar' => [ 'Vendor\\Bar', 'Vendor\\Deep\\Bar' ]
121 | ]);
122 |
123 | // Is the same as this
124 | $architecture->component('Foo')
125 | ->identifiedByNamespace('Vendor\\Foo')
126 | ->component('Bar')
127 | ->identifierByNamespace('Vendor\\Bar')
128 | ->identifiedByNamespace('Vendor\\Deep\\Bar')
129 | ```
130 |
131 | ### Syntactic sugar: Chaining multiple dependency rules
132 | If a non-existing component is referenced in one of these methods then it will be created.
133 | These methods will also set the referenced component as the currently active one - so when using
134 | `->mustNotDependOn('FooBar')` all future operations reference the `FooBar` component.
135 |
136 | In order to chain multiple dependency rules for a single component there are some convenience
137 | methods available:
138 |
139 | - `andMustNotDependOn`
140 | - `andMustNotBeDependedOnBy`
141 |
142 | ```php
143 | // This
144 | (new Architecture)
145 | ->component('Foo')
146 | ->mustNotDependOn('Bar')
147 | ->andMustNotDependOn('Baz')
148 |
149 | // Is this same as this:
150 | (new Architecture())
151 | ->component('Foo')->mustNotDependOn('Bar')
152 | ->component('Foo')->mustNotDependOn('Baz')
153 | ```
154 |
155 | ### Shorthand for monorepos: `addComposerBasedComponent`
156 |
157 | In case one repository contains multiple packages that all have their own `composer.json`
158 | file it is easy to accidentally use a method or class of something that is not in the `composer.json`
159 | file of the current package.
160 |
161 | To prevent this the `Architecture->mustOnlyDependOnComposerDependencies` method and the
162 | `MustOnlyDependOnComposerDependencies` validator can be used to check if all used namespaces are
163 | declared in a given `composer.json` file:
164 |
165 | ```php
166 | $architecture = (new Architecture)
167 | ->component('vendor/subpackage')
168 | ->identifierByNamespace('Vendor\\Subpackage\\')
169 | ->mustOnlyDependOnComposerDependencies('packages/subpackage/composer.json');
170 | ```
171 |
172 | However, `composer.json` already contains information about the package name and namespaces.
173 | Therefore the `addComposerBasedComponent` method can be used in order to make
174 | things easier:
175 |
176 | ```php
177 | $architecture = (new Architecture)
178 | ->addComposerBasedComponent('packages/subpackage/composer.json');
179 | ```
180 |
181 | ## Examples
182 |
183 | - [PHPArch tests its own architecture](./tests/ArchitectureTest.php)
184 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "j6s/phparch",
3 | "description": "Architecture Testing for PHP projects",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Johannes Hertenstein",
8 | "email": "phparch@thej6s.com"
9 | }
10 | ],
11 | "require": {
12 | "php": "^7.4|^8.0",
13 |
14 | "nikic/php-parser": "^4.0",
15 | "symfony/finder": "^4.1|^5.0|^6.0",
16 | "phpdocumentor/reflection-docblock": "^5.2.2",
17 | "phpdocumentor/type-resolver": "^1.4",
18 | "thecodingmachine/safe": "^1.3|^2.0"
19 | },
20 | "require-dev": {
21 | "phpunit/phpunit": "^9.4",
22 | "phpstan/phpstan": "^1.5.1",
23 | "squizlabs/php_codesniffer": "^3.5"
24 | },
25 | "suggest": {
26 | "phpunit/phpunit": "For assertion helpers to work correctly."
27 | },
28 | "autoload": {
29 | "psr-4": {
30 | "J6s\\PhpArch\\": "src/"
31 | }
32 | },
33 | "autoload-dev": {
34 | "psr-4": {
35 | "J6s\\PhpArch\\Tests\\": "tests/"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | src/
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 8
3 | paths:
4 | - src
5 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 | ./tests
15 |
16 |
17 |
18 |
19 |
20 | ./src
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Component/Architecture.php:
--------------------------------------------------------------------------------
1 | composerFileParserFactory = new ComposerFileParserFactory();
28 | }
29 |
30 | /**
31 | * @return $this
32 | */
33 | public function withComposerFileParserFactory(ComposerFileParserFactoryInterface $factory): self
34 | {
35 | $this->composerFileParserFactory = $factory;
36 | return $this;
37 | }
38 |
39 | /**
40 | * Adds or selects a component that is identified by the given name.
41 | * Any subsequent declarations of dependencies reference the component with that name.
42 | *
43 | * @return $this
44 | */
45 | public function component(string $name): self
46 | {
47 | $this->setCurrent(
48 | $this->ensureComponentExists($name)
49 | );
50 | return $this;
51 | }
52 |
53 | /**
54 | * Declares components based on the given associative array.
55 | * The given definitions must be a mapping from the component name to the namespaces
56 | * defining that component.
57 | *
58 | * @example
59 | * // This
60 | * $architecture->components([
61 | * 'Foo' => 'Vendor\\Foo',
62 | * 'Bar' => [ 'Vendor\\Bar', 'Vendor\\Deep\\Bar' ]
63 | * ]);
64 | * // Is the same as this
65 | * $architecture->component('Foo')->identifiedByNamespace('Vendor\\Foo')
66 | * ->component('Bar')->identifierByNamespace('Vendor\\Bar')->identifiedByNamespace('Vendor\\Deep\\Bar')
67 | *
68 | * @param string[]|string[][] $definitions
69 | * @return $this
70 | * @throws ComponentNotDefinedException
71 | */
72 | public function components(array $definitions): self
73 | {
74 | $currentComponent = $this->currentComponent;
75 | $lastComponent = $this->lastComponent;
76 |
77 | foreach ($definitions as $name => $identifiedBy) {
78 | if (!is_array($identifiedBy)) {
79 | $identifiedBy = [ $identifiedBy ];
80 | }
81 |
82 | $this->component($name);
83 | foreach ($identifiedBy as $namespace) {
84 | $this->identifiedByNamespace($namespace);
85 | }
86 | }
87 |
88 | $this->currentComponent = $currentComponent;
89 | $this->lastComponent = $lastComponent;
90 | return $this;
91 | }
92 |
93 | /**
94 | * Defines that the currently selected component is identified by the given namespace.
95 | * This method can be called multiple times in order to add multiple namespaces to the component.
96 | *
97 | * @param string $namespace
98 | * @return $this
99 | * @throws ComponentNotDefinedException
100 | */
101 | public function identifiedByNamespace(string $namespace): self
102 | {
103 | $this->getCurrent()->addNamespace($namespace);
104 | return $this;
105 | }
106 |
107 | /**
108 | * Declares that the currently selected component must not depend on by the component
109 | * with the given name. The declaration of this rule can be made before the second component
110 | * is defined.
111 | *
112 | * @example
113 | * (new Architecture)
114 | * ->component('Logic')->identifiedByNamespace('App\\Logic')
115 | * ->mustNotDependOn('IO')->identifiedByNamespace('App\\IO')
116 | *
117 | * @return $this
118 | * @throws ComponentNotDefinedException
119 | */
120 | public function mustNotDependOn(string $name): self
121 | {
122 | $component = $this->ensureComponentExists($name);
123 | $this->getCurrent()->mustNotDependOn($component);
124 | $this->setCurrent($component);
125 | return $this;
126 | }
127 |
128 | /**
129 |
130 | * Declares that the currently selected component must not depend on by the component
131 | * with the given name ignoring interfaces. The declaration of this rule can be made
132 | * before the second component is defined.
133 | *
134 | * @example
135 | * (new Architecture)
136 | * ->component('Logic')->identifiedByNamespace('App\\Logic')
137 | * ->mustNotDirectlyDependOn('IO')->identifiedByNamespace('App\\IO')
138 | *
139 | * @return $this
140 | * @throws ComponentNotDefinedException
141 | */
142 | public function mustNotDirectlyDependOn(string $name): self
143 | {
144 | $component = $this->ensureComponentExists($name);
145 | $this->getCurrent()->mustNotDependOn($component, true);
146 | $this->setCurrent($component);
147 | return $this;
148 | }
149 |
150 | /**
151 | * Same as `mustNotDependOn` but refenrences the previous component.
152 | * This is helpful for 'speaking' chaining.
153 | *
154 | * @example
155 | * (new Architecture)
156 | * ->component('Logic')->identifiedByNamespace('App\\Logic')
157 | * ->mustNotDependOn('IO')
158 | * ->andMustNotDependOn('Controllers')
159 | *
160 | *
161 | * @return $this
162 | * @throws ComponentNotDefinedException
163 | */
164 | public function andMustNotDependOn(string $name): self
165 | {
166 | $this->restoreLast();
167 | return $this->mustNotDependOn($name);
168 | }
169 |
170 | /**
171 | * Declares that the currently selected component must not be depended on by the component
172 | * with the given name. The declaration of this rule can be made before the second component
173 | * is defined.
174 | *
175 | * @example
176 | * (new Architecture)
177 | * ->component('IO')->identifiedByNamespace('App\\IO')
178 | * ->mustNotBeDependedOnBy('Logic')->identifiedBy('App\\Logic');
179 | *
180 | * @return $this
181 | * @throws ComponentNotDefinedException
182 | */
183 | public function mustNotBeDependedOnBy(string $name): self
184 | {
185 | $component = $this->ensureComponentExists($name);
186 | $component->mustNotDependOn($this->getCurrent());
187 | $this->setCurrent($component);
188 | return $this;
189 | }
190 |
191 |
192 | /**
193 | * Same as `mustNotBeDependedOnBy` but refenrences the previous component.
194 | * This is helpful for 'speaking' chaining.
195 | *
196 | * @example
197 | * @example
198 | * (new Architecture)
199 | * ->component('IO')->identifiedByNamespace('App\\IO')
200 | * ->mustNotBeDependedOnBy('Logic')
201 | * ->andMustNotBeDependedOnBy('Controllers')
202 | *
203 | * @return $this
204 | * @throws ComponentNotDefinedException
205 | */
206 | public function andMustNotBeDependedOnBy(string $name): self
207 | {
208 | $this->restoreLast();
209 | return $this->mustNotBeDependedOnBy($name);
210 | }
211 |
212 | /**
213 | * Declares that the currently selected component must only depend on the component with the
214 | * given name or itself.
215 | *
216 | * @return $this
217 | * @throws ComponentNotDefinedException
218 | */
219 | public function mustOnlyDependOn(string $name): self
220 | {
221 | $component = $this->ensureComponentExists($name);
222 | $this->getCurrent()->mustOnlyDependOn($component);
223 | $this->setCurrent($component);
224 | return $this;
225 | }
226 |
227 |
228 | /**
229 | * Ensures that the current component only depends on namespaces that are declared
230 | * in the given composer & lock files.
231 | *
232 | * If no lock file is passed then the name is automatically generated based on the
233 | * composer file name.
234 | *
235 | * @example
236 | * $monorepo = (new Architecture)->components([
237 | * 'PackageOne' => 'Vendor\\Library\\PackageOne',
238 | * 'PackageTwo' => 'Vendor\\Library\\PackageTwo',
239 | * ]);
240 | *
241 | * $monorepo->component('PackageOne')->mustOnlyDependOnComposerDependencies('Packages/PackageOne/composer.json');
242 | * $monorepo->component('PackageTwo')->mustOnlyDependOnComposerDependencies('Packages/PackageTwo/composer.json');
243 | *
244 | * @return $this
245 | * @throws ComponentNotDefinedException
246 | */
247 | public function mustOnlyDependOnComposerDependencies(
248 | string $composerFile,
249 | ?string $lockFile = null,
250 | bool $includeDev = false
251 | ): self {
252 | $this->getCurrent()->mustOnlyDependOnComposerDependencies(
253 | $this->composerFileParserFactory->create($composerFile, $lockFile),
254 | $includeDev
255 | );
256 | return $this;
257 | }
258 |
259 | /**
260 | * Adds a new composer based component.
261 | * In an composer based component the namespaces and dependencies are automatically read from
262 | * the composer.json file supplied.
263 | *
264 | * @example
265 | * $monorepo = (new Architecture)
266 | * ->addComposerBasedComponent('Packages/PackageOne/composer.json')
267 | * ->addComposerBasedComponent('Packages/PackageTwo/composer.json');
268 | *
269 | * @return $this
270 | * @throws ComponentNotDefinedException
271 | */
272 | public function addComposerBasedComponent(
273 | string $composerFile,
274 | ?string $lockFile = null,
275 | string $componentName = null,
276 | bool $includeDev = false
277 | ): self {
278 | $parser = $this->composerFileParserFactory->create($composerFile, $lockFile);
279 | $this->component($componentName ?? $parser->getName());
280 |
281 | foreach ($parser->getNamespaces() as $namespace) {
282 | $this->getCurrent()->addNamespace($namespace);
283 | }
284 |
285 | $this->getCurrent()->mustOnlyDependOnComposerDependencies($parser, $includeDev);
286 | return $this;
287 | }
288 |
289 |
290 | /**
291 | * Allows adding exceptions to previously declared rules.
292 | *
293 | * @example
294 | * $architecture = new Architecture([
295 | * 'PackageOne' => 'Vendor\\Namespace\\PackageOne',
296 | * 'PackageTwo' => 'Vendor\\Namespace\\PackageTwo',
297 | * 'PackageThree' => 'Vendor\\Namespace\\PackageThree'
298 | * ]);
299 | *
300 | * $architecture->disallowInterdependence([ 'PackageOne', 'PackageTwo', 'PackageThree' ]);
301 | * $architecture->component('PackageThree')->isAllowedToDependOn('PackageOne');
302 | *
303 | *
304 | * @return $this
305 | * @throws ComponentNotDefinedException
306 | */
307 | public function isAllowedToDependOn(string $component): self
308 | {
309 | $this->getCurrent()->explicitlyAllowDependency($this->ensureComponentExists($component));
310 | return $this;
311 | }
312 |
313 |
314 | /**
315 | * Method to declare interdependence:
316 | * All components in the first array must not depend on any other components in that array.
317 | *
318 | * The second key-value array can be used to explicitly allow some dependencies among them.
319 | *
320 | * Note: The second key-value array only allows dependencies in this specific context.
321 | * If you want to declare a more broadly applicable allowance then the
322 | * {@see Architecture::isAllowedToDependOn} is worth looking at.
323 | * Note: This method assumes that all components have been declared before
324 | * (e.g. using the {@see Architecture::components} method).
325 | *
326 | * @example
327 | * $architecture->components([
328 | * 'Foo' => 'App\\Foo',
329 | * 'Bar' => 'App\\Bar',
330 | * 'Baz' => 'App\\Baz',
331 | * ]);
332 | *
333 | * // No dependencies between all 3 components allowed - except for Foo => Baz.
334 | * $architecture->disallowInterdependence(
335 | * [ 'Foo', 'Bar', 'Baz' ],
336 | * [ 'Foo' => [ 'Baz' ] ]
337 | * );
338 | *
339 | * @param string[] $components
340 | * @param string[][] $allowed
341 | * @return $this
342 | * @throws ComponentNotDefinedException
343 | */
344 | public function disallowInterdependence(array $components, array $allowed = []): self
345 | {
346 | ArrayUtility::forEachCombinationInArray($components, function (string $source, string $target) use ($allowed) {
347 | if (\in_array($target, $allowed[$source] ?? [], true)) {
348 | return;
349 | }
350 | $this->component($source)->mustNotDependOn($target);
351 | });
352 |
353 | return $this;
354 | }
355 |
356 | /**
357 | * Declares that the current component must not depend on any other previously defined components.
358 | *
359 | * Note: This method assumes that all components have been declared before
360 | * (e.g. using the {@see Architecture::components} method).
361 | *
362 | * @example
363 | * $architecture->components([
364 | * 'Core' => 'App\\Core',
365 | * 'Utilities' => 'App\\Utility',
366 | * 'Events' => 'App\\Event',
367 | * ]);
368 | *
369 | * $architecture->component('Core')->mustNotDependOnAnyOtherComponent();
370 | *
371 | * @return $this
372 | * @throws ComponentNotDefinedException
373 | */
374 | public function mustNotDependOnAnyOtherComponent(): self
375 | {
376 | $current = $this->getCurrent();
377 | foreach ($this->components as $component) {
378 | if ($current !== $component) {
379 | $current->mustNotDependOn($component);
380 | }
381 | }
382 | return $this;
383 | }
384 |
385 | private function getCurrent(): Component
386 | {
387 | if ($this->currentComponent === null) {
388 | throw new ComponentNotDefinedException('No current component exists');
389 | }
390 | return $this->currentComponent;
391 | }
392 |
393 | private function ensureComponentExists(string $name): Component
394 | {
395 | if (!array_key_exists($name, $this->components)) {
396 | $this->components[$name] = new Component($name);
397 | $this->addValidator($this->components[$name]);
398 | }
399 | return $this->components[$name];
400 | }
401 |
402 | private function restoreLast(): void
403 | {
404 | $this->currentComponent = $this->lastComponent;
405 | $this->lastComponent = null;
406 | }
407 |
408 | private function setCurrent(Component $component): void
409 | {
410 | $this->lastComponent = $this->currentComponent;
411 | $this->currentComponent = $component;
412 | }
413 | }
414 |
--------------------------------------------------------------------------------
/src/Component/Component.php:
--------------------------------------------------------------------------------
1 | [] */
30 | private array $rules = [];
31 |
32 | /** @var Component[] */
33 | private $explicitlyAllowed = [];
34 |
35 | public function __construct(string $name)
36 | {
37 | parent::__construct();
38 | $this->name = $name;
39 | }
40 |
41 | public function mustNotDependOn(Component $component, bool $allowInterfaces = false): void
42 | {
43 | $this->rules[] = [
44 | 'component' => $component,
45 | 'type' => self::MUST_NOT_DEPEND_ON,
46 | 'allowInterfaces' => $allowInterfaces,
47 | ];
48 | }
49 |
50 | public function mustOnlyDependOn(Component $component): void
51 | {
52 | $this->rules[] = [
53 | 'component' => $component,
54 | 'type' => self::MUST_ONLY_DEPEND_ON,
55 | 'allowInterfaces' => false
56 | ];
57 | }
58 |
59 | public function mustOnlyDependOnComposerDependencies(
60 | ComposerFileParserInterface $parser,
61 | bool $includeDev = false
62 | ): void {
63 | $this->rules[] = [
64 | 'type' => self::MUST_ONLY_DEPEND_ON_COMPOSER_DEPENDENCIES,
65 | 'parser' => $parser,
66 | 'includeDev' => $includeDev
67 | ];
68 | }
69 |
70 | public function explicitlyAllowDependency(Component $component): void
71 | {
72 | $this->explicitlyAllowed[] = $component;
73 | }
74 |
75 | /** @return string[] */
76 | public function getNamespaces(): array
77 | {
78 | return $this->namespaces;
79 | }
80 |
81 | public function addNamespace(string $namespace): void
82 | {
83 | $this->namespaces[] = $namespace;
84 | }
85 |
86 | public function __toString(): string
87 | {
88 | return 'Component(' . $this->name . ')';
89 | }
90 |
91 | /** @return Validator[] */
92 | protected function getValidators(): array
93 | {
94 | $collection = new ValidationCollection();
95 | foreach ($this->rules as $rule) {
96 | $collection->addValidator($this->ruleToValidator($rule));
97 | }
98 |
99 | foreach ($this->explicitlyAllowed as $component) {
100 | foreach ($this->namespaces as $fromNamespace) {
101 | $collection = new ExplicitlyAllowDependency(
102 | $collection,
103 | $fromNamespace,
104 | $component->getNamespaces()
105 | );
106 | }
107 | }
108 |
109 | return [ $collection ];
110 | }
111 |
112 | /** @param array $rule */
113 | private function ruleToValidator(array $rule): Validator
114 | {
115 | $type = $rule['type'] ?? '[UNKNOWN]';
116 |
117 | switch ($type) {
118 | case self::MUST_NOT_DEPEND_ON:
119 | $validator = $this->mustNotDependOnValidator($rule['component']);
120 | break;
121 | case self::MUST_ONLY_DEPEND_ON:
122 | $validator = $this->mustOnlyDependOnValidator($rule['component']);
123 | break;
124 | case self::MUST_ONLY_DEPEND_ON_COMPOSER_DEPENDENCIES:
125 | $validator = $this->mustOnlyDependOnComposerDependenciesValidator(
126 | $rule['parser'],
127 | $rule['includeDev'] ?? false
128 | );
129 | break;
130 | default:
131 | throw new \InvalidArgumentException('Cannot build rule of type ' . $type);
132 | }
133 |
134 |
135 | if ($rule['allowInterfaces'] ?? false) {
136 | $validator = new AllowInterfaces($validator);
137 | }
138 |
139 | return $validator;
140 | }
141 |
142 | private function mustNotDependOnValidator(Component $component): Validator
143 | {
144 | $validators = new ValidationCollection();
145 | foreach ($this->namespaces as $fromNamespace) {
146 | foreach ($component->getNamespaces() as $toNamespace) {
147 | $validators->addValidator(new ForbiddenDependency(
148 | $fromNamespace,
149 | $toNamespace,
150 | $this . ' must not depend on ' . $component .
151 | ' but :violatingFrom depends on :violatingTo'
152 | ));
153 | }
154 | }
155 | return $validators;
156 | }
157 |
158 | private function mustOnlyDependOnValidator(Component $component): Validator
159 | {
160 | $validators = new ValidationCollection();
161 | foreach ($this->namespaces as $fromNamespace) {
162 | foreach ($component->getNamespaces() as $toNamespace) {
163 | $validators->addValidator(new MustOnlyDependOn(
164 | $fromNamespace,
165 | $toNamespace,
166 | $this . ' must only depend on ' . $component .
167 | ' but :violatingFrom depends on :violatingTo'
168 | ));
169 | }
170 | }
171 | return $validators;
172 | }
173 |
174 | private function mustOnlyDependOnComposerDependenciesValidator(
175 | ComposerFileParserInterface $parser,
176 | bool $includeDev
177 | ): Validator {
178 | $validators = new ValidationCollection();
179 | foreach ($this->namespaces as $fromNamespace) {
180 | $validators->addValidator(new MustOnlyDependOnComposerDependencies($fromNamespace, $parser, $includeDev));
181 | }
182 | return $validators;
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/src/Composer/CachedComposerFileParserFactory.php:
--------------------------------------------------------------------------------
1 | */
10 | private array $composerFileParsersCache = [];
11 |
12 | public function create(string $composerFile, string $lockFile = null): ComposerFileParserInterface
13 | {
14 | $key = sprintf('%s|%s', $composerFile, $lockFile ?? 'default');
15 |
16 | if (!array_key_exists($key, $this->composerFileParsersCache)) {
17 | $this->composerFileParsersCache[$key] = new ComposerFileParserCacheDecorator(
18 | new ComposerFileParser(
19 | $composerFile,
20 | $lockFile
21 | )
22 | );
23 | }
24 |
25 | return $this->composerFileParsersCache[$key];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Composer/ComposerFileParser.php:
--------------------------------------------------------------------------------
1 | */
14 | private array $composerFile;
15 |
16 | private string $lockFilePath;
17 |
18 | /** @var array */
19 | private array $lockFile;
20 |
21 | /** @var array */
22 | private array $lockedPackages;
23 |
24 | public function __construct(string $composerFile, string $lockFile = null)
25 | {
26 | if ($lockFile === null) {
27 | $lockFile = substr($composerFile, 0, -5) . '.lock';
28 | }
29 |
30 | $this->composerFile = json_decode(file_get_contents($composerFile), true);
31 | $this->composerFilePath = $composerFile;
32 | $this->lockFile = json_decode(file_get_contents($lockFile), true);
33 | $this->lockFilePath = $lockFile;
34 | $this->lockedPackages = $this->getPackagesFromLockFile();
35 | }
36 |
37 | /**
38 | * Returns an array of all namespaces declared by the current composer file.
39 | *
40 | * @return string[]
41 | */
42 | public function getNamespaces(bool $includeDev = false): array
43 | {
44 | return $this->extractNamespaces($this->composerFile, $includeDev);
45 | }
46 |
47 | /**
48 | * Returns an array of all required namespaces including deep dependencies (dependencies of dependencies)
49 | *
50 | * @return string[]
51 | */
52 | public function getDeepRequirementNamespaces(bool $includeDev): array
53 | {
54 | $required = $this->getDirectDependencies($includeDev);
55 | $required = $this->flattenDependencies($required, $includeDev);
56 | return $this->autoloadableNamespacesForRequirements($required, $includeDev);
57 | }
58 |
59 | /**
60 | * Returns an array of directly required package names.
61 | *
62 | * @return string[]
63 | */
64 | public function getDirectDependencies(bool $includeDev): array
65 | {
66 | $required = [];
67 | foreach (array_keys($this->composerFile['require'] ?? []) as $packageName) {
68 | $required[] = (string) $packageName;
69 | }
70 |
71 | if ($includeDev) {
72 | foreach (array_keys($this->composerFile['require-dev'] ?? []) as $packageName) {
73 | $required[] = (string) $packageName;
74 | }
75 | }
76 |
77 | return $required;
78 | }
79 |
80 | /**
81 | * Resolves an array of package names to an array of namespaces declared by those packages.
82 | *
83 | * @param string[] $requirements
84 | * @return string[]
85 | */
86 | public function autoloadableNamespacesForRequirements(array $requirements, bool $includeDev): array
87 | {
88 | $namespaces = [ [] ];
89 |
90 | foreach ($requirements as $package) {
91 | $namespaces[] = $this->extractNamespaces($this->lockedPackages[$package], $includeDev);
92 | }
93 |
94 | return array_merge(...$namespaces);
95 | }
96 |
97 | public function getComposerFilePath(): string
98 | {
99 | return $this->composerFilePath;
100 | }
101 |
102 | public function getLockFilePath(): string
103 | {
104 | return $this->lockFilePath;
105 | }
106 |
107 | public function getName(): string
108 | {
109 | return $this->composerFile['name'];
110 | }
111 |
112 | /**
113 | * @param mixed[] $topLevelRequirements
114 | * @return mixed[]
115 | */
116 | private function flattenDependencies(array $topLevelRequirements, bool $includeDev): array
117 | {
118 | $required = [];
119 | $toCheck = $topLevelRequirements;
120 |
121 | while (\count($toCheck) > 0) {
122 | $packageName = array_pop($toCheck);
123 | $package = $this->lockedPackages[$packageName] ?? null;
124 | if ($package === null) {
125 | continue;
126 | }
127 |
128 | $required[] = $packageName;
129 |
130 | $deepRequirements = array_keys($package['require'] ?? []);
131 | if ($includeDev) {
132 | $deepRequirements = array_merge(
133 | $deepRequirements,
134 | array_keys($package['require-dev'] ?? [])
135 | );
136 | }
137 |
138 | foreach ($deepRequirements as $name) {
139 | if (!\in_array($name, $required)) {
140 | $toCheck[] = $name;
141 | }
142 | }
143 | }
144 |
145 | return $required;
146 | }
147 |
148 | /**
149 | * @return array
150 | */
151 | private function getPackagesFromLockFile(): array
152 | {
153 | $lockedPackages = [];
154 |
155 | foreach ($this->lockFile['packages'] ?? [] as $package) {
156 | $lockedPackages[(string) $package['name']] = $package;
157 | }
158 |
159 | foreach ($this->lockFile['packages-dev'] ?? [] as $package) {
160 | $lockedPackages[(string) $package['name']] = $package;
161 | }
162 |
163 | return $lockedPackages;
164 | }
165 |
166 | /**
167 | * @param array $package
168 | * @return string[]
169 | */
170 | private function extractNamespaces(array $package, bool $includeDev): array
171 | {
172 | $namespaces = [];
173 | foreach (array_keys($package['autoload']['psr-0'] ?? []) as $namespace) {
174 | $namespaces[] = (string) $namespace;
175 | }
176 | foreach (array_keys($package['autoload']['psr-4'] ?? []) as $namespace) {
177 | $namespaces[] = (string) $namespace;
178 | }
179 |
180 | if ($includeDev) {
181 | foreach (array_keys($package['autoload-dev']['psr-0'] ?? []) as $namespace) {
182 | $namespaces[] = (string) $namespace;
183 | }
184 | foreach (array_keys($package['autoload-dev']['psr-4'] ?? []) as $namespace) {
185 | $namespaces[] = (string) $namespace;
186 | }
187 | }
188 |
189 | return $namespaces;
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/Composer/ComposerFileParserCacheDecorator.php:
--------------------------------------------------------------------------------
1 | decorated = $decorated;
13 | }
14 |
15 | private const KEY_WITH_DEV = 'with_dev';
16 | private const KEY_WITHOUT_DEV = 'without_dev';
17 |
18 | /**
19 | * @var array
20 | * @phpstan-var array
21 | */
22 | private array $deepRequirementNamespaces = [];
23 |
24 | /**
25 | * @var array
26 | * @phpstan-var array
27 | */
28 | private array $namespaces = [];
29 |
30 | /**
31 | * @var array
32 | * @phpstan-var array
33 | */
34 | private array $directDependencies = [];
35 |
36 | public function getNamespaces(bool $includeDev = false): array
37 | {
38 | $key = $includeDev ? self::KEY_WITH_DEV : self::KEY_WITHOUT_DEV;
39 | if (!array_key_exists($key, $this->namespaces)) {
40 | $this->namespaces[$key] = $this->decorated->getNamespaces($includeDev);
41 | }
42 |
43 | return $this->namespaces[$key];
44 | }
45 |
46 | public function getDirectDependencies(bool $includeDev): array
47 | {
48 | $key = $includeDev ? self::KEY_WITH_DEV : self::KEY_WITHOUT_DEV;
49 | if (!array_key_exists($key, $this->directDependencies)) {
50 | $this->directDependencies[$key] = $this->decorated->getDirectDependencies($includeDev);
51 | }
52 |
53 | return $this->directDependencies[$key];
54 | }
55 |
56 |
57 | public function getDeepRequirementNamespaces(bool $includeDev): array
58 | {
59 | $key = $includeDev ? self::KEY_WITH_DEV : self::KEY_WITHOUT_DEV;
60 | if (!array_key_exists($key, $this->deepRequirementNamespaces)) {
61 | $this->deepRequirementNamespaces[$key] = $this->decorated->getDeepRequirementNamespaces($includeDev);
62 | }
63 |
64 | return $this->deepRequirementNamespaces[$key];
65 | }
66 |
67 | public function autoloadableNamespacesForRequirements(array $requirements, bool $includeDev): array
68 | {
69 | return $this->decorated->autoloadableNamespacesForRequirements($requirements, $includeDev);
70 | }
71 |
72 | public function getComposerFilePath(): string
73 | {
74 | return $this->decorated->getComposerFilePath();
75 | }
76 |
77 | public function getLockFilePath(): string
78 | {
79 | return $this->decorated->getLockFilePath();
80 | }
81 |
82 | public function getName(): string
83 | {
84 | return $this->decorated->getName();
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Composer/ComposerFileParserFactory.php:
--------------------------------------------------------------------------------
1 | declaredNamespace;
25 | }
26 |
27 | /** @return string[] */
28 | public function getUsedNamespaces(): array
29 | {
30 | return $this->usedNamespaces;
31 | }
32 |
33 | /** @param array $ast */
34 | public function process(array $ast): void
35 | {
36 | $declared = new ExtractDeclaredNamespace();
37 | $used = $this->usageExtractors();
38 |
39 | $this->traverseWithVisitors($ast, array_merge($used, [ $declared ]));
40 |
41 | $this->declaredNamespace = $declared->getDeclared();
42 | foreach ($used as $visitor) {
43 | foreach ($visitor->getNamespaces() as $namespace) {
44 | if (!\in_array($namespace, $this->usedNamespaces, true)) {
45 | $this->usedNamespaces[] = $namespace;
46 | }
47 | }
48 | }
49 | }
50 |
51 | /**
52 | * @param array $ast
53 | * @param NodeVisitor[] $visitors
54 | */
55 | private function traverseWithVisitors(array $ast, array $visitors): void
56 | {
57 | $traverser = new NodeTraverser();
58 | $traverser->addVisitor(new NameResolver());
59 | foreach ($visitors as $visitor) {
60 | $traverser->addVisitor($visitor);
61 | }
62 | $traverser->traverse($ast);
63 | }
64 |
65 | /** @return NamespaceCollectingVisitor[] */
66 | private function usageExtractors(): array
67 | {
68 | return [
69 | new FullyQualifiedReference(),
70 | new UseStatement(),
71 | new DocBlockTypeAnnotations(),
72 | ];
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Parser/ParserException.php:
--------------------------------------------------------------------------------
1 | lastNamespace = $node->name ? $node->name->toString() : '';
32 | $this->useStatements = [];
33 | } elseif ($node instanceof Node\Stmt\UseUse) {
34 | $this->useStatements[$this->extractAlias($node)] = $node->name->toString();
35 | } elseif ($node instanceof Node\Stmt\ClassMethod) {
36 | $context = new Context($this->lastNamespace, $this->useStatements);
37 | if ($node->hasAttribute('comments')) {
38 | $this->extractDocBlocks((array)$node->getAttribute('comments'), $context);
39 | }
40 | }
41 | return null;
42 | }
43 |
44 | /** @param Doc[] $docBlocks */
45 | private function extractDocBlocks(array $docBlocks, Context $context): void
46 | {
47 | $factory = DocBlockFactory::createInstance();
48 |
49 | foreach ($docBlocks as $docBlockString) {
50 | try {
51 | $docBlock = $factory->create((string) $docBlockString);
52 | } catch (\InvalidArgumentException|\RuntimeException $e) {
53 | throw new ParserException(
54 | sprintf("Error parsing dockblock \n\n %s \n\n %s", $docBlockString, $e->getMessage()),
55 | $e->getCode(),
56 | $e
57 | );
58 | }
59 |
60 | $fqsenAlias = [];
61 |
62 | foreach ($docBlock->getTags() as $tag) {
63 | // Template tags are parsed to local aliases
64 | if (($tag instanceof Generic) && $tag->getName() === 'template') {
65 | [ $source, $target ] = $this->parseTemplateTag((string) $tag->getDescription(), $context);
66 | if ($source !== null) {
67 | $fqsenAlias[$source] = $target;
68 | }
69 | }
70 |
71 | if (($tag instanceof TagWithType) && $tag->getType() !== null) {
72 | $type = $tag->getType();
73 |
74 | foreach ($this->flattenTypes($type) as $typeToResolve) {
75 | if (!($typeToResolve instanceof Object_)) {
76 | continue;
77 | }
78 | $type = $this->typeToFullyQualified((string) $typeToResolve, $context);
79 | if ($type && array_key_exists($type, $fqsenAlias)) {
80 | $type = $fqsenAlias[$type];
81 | }
82 |
83 | if ($type) {
84 | $this->namespaces[] = $type;
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 |
92 | /**
93 | * Returns an array that always contains 2 elements: The source and the target type.
94 | * If the target type is `null`, then this is an untyped template.
95 | * If both, source and target are `null`, then the template tag may be invalid.
96 | *
97 | * @param string $contents
98 | * @return (string|null)[]
99 | */
100 | private function parseTemplateTag(string $contents, Context $context): array
101 | {
102 | $parts = preg_split('/\s+/', $contents);
103 |
104 | // First part is always the name
105 | $name = $parts[0] ?? null;
106 | $value = null;
107 |
108 | // Typed template: Has 3 parts of format `TemplateName of BaseType`
109 | if (\count($parts) === 3 && $parts[1] === 'of') {
110 | $value = $parts[2];
111 | }
112 |
113 | return [
114 | $name ? $this->typeToFullyQualified($name, $context) : null,
115 | $value ? $this->typeToFullyQualified($value, $context) : null,
116 | ];
117 | }
118 |
119 | /**
120 | * @return Type[]
121 | */
122 | private function flattenTypes(Type $type): array
123 | {
124 | // Most basic type
125 | if ($type instanceof Object_) {
126 | return [ $type ];
127 | }
128 |
129 | // Array types are comprised of their key & value pairs
130 | if ($type instanceof Array_) {
131 | return array_merge(
132 | $this->flattenTypes($type->getKeyType()),
133 | $this->flattenTypes($type->getValueType()),
134 | );
135 | }
136 |
137 | // To resolve generic definitions correctly we have to split the type into its original
138 | // type and the value part.
139 | if ($type instanceof Collection) {
140 | return array_merge(
141 | $this->flattenTypes(new Object_($type->getFqsen())),
142 | $this->flattenTypes($type->getKeyType()),
143 | $this->flattenTypes($type->getValueType()),
144 | );
145 | }
146 |
147 | // Types that consist of multiple parts (e.g. union types: A|B)
148 | if ($type instanceof AggregatedType) {
149 | $typeList = [];
150 | foreach ($type->getIterator() as $innerType) {
151 | $typeList[] = $this->flattenTypes($innerType);
152 | }
153 | return array_merge(...$typeList);
154 | }
155 |
156 | return [];
157 | }
158 |
159 | private function typeToFullyQualified(string $type, Context $context): ?string
160 | {
161 | if (empty(trim($type))) {
162 | return null;
163 | }
164 |
165 | // Try to resolve relative to current namespace first
166 | $resolvedType = (string) (new TypeResolver())->resolve(ltrim($type, '\\'), $context);
167 | if (class_exists($resolvedType) || interface_exists($resolvedType) || trait_exists($resolvedType)) {
168 | return ltrim($resolvedType, '\\');
169 | }
170 |
171 | // Assume absolute reference else
172 | return ltrim((string) $type, '\\');
173 | }
174 |
175 | private function extractAlias(Node\Stmt\UseUse $node): string
176 | {
177 | if (!method_exists($node, 'getAlias')) {
178 | // Compatibility mode: nikic/php-parser@3.x
179 | return (string) $node->alias;
180 | }
181 |
182 | return $node->getAlias()->toString();
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/src/Parser/Visitor/ExtractDeclaredNamespace.php:
--------------------------------------------------------------------------------
1 | namespacedName) && $node->namespacedName !== null) {
17 | $this->declared = $node->namespacedName->toString();
18 | }
19 | return null;
20 | }
21 |
22 | public function declaresNamespace(): bool
23 | {
24 | return $this->declared !== '';
25 | }
26 |
27 | public function getDeclared(): string
28 | {
29 | return trim($this->declared, '\\');
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Parser/Visitor/FullyQualifiedReference.php:
--------------------------------------------------------------------------------
1 | isCallToSimpleFunction($node)) {
22 | $this->namespaces[] = (string) $node;
23 | }
24 | return null;
25 | }
26 |
27 | private function isCallToSimpleFunction(FullyQualified $node): bool
28 | {
29 | return function_exists((string) $node);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Parser/Visitor/NamespaceCollectingVisitor.php:
--------------------------------------------------------------------------------
1 | namespaces;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Parser/Visitor/UseStatement.php:
--------------------------------------------------------------------------------
1 | namespaces[] = $node->name->toString();
15 | }
16 | return null;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/PhpArch.php:
--------------------------------------------------------------------------------
1 | validator = new ValidationCollection();
24 | }
25 |
26 | /**
27 | * Asserts that the currently configured validations have no errors.
28 | * This method is intended to be used inside of a PHPUnit test case and
29 | * will only work if PHPUnit was installed separately from phparch.
30 | */
31 | public function assertHasNoErrors(): void
32 | {
33 | $errors = $this->errors();
34 | Assert::assertEmpty($errors, sprintf(
35 | "%s \n\n\t %d errors occurred while validating architecture\n",
36 | implode("\n", $errors),
37 | \count($errors)
38 | ));
39 | }
40 |
41 | /**
42 | * Executes the validations and returns an array with all errors.
43 | *
44 | * @return string[]
45 | */
46 | public function errors(): array
47 | {
48 | $errors = [ [] ];
49 | $phpParser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
50 | $finder = $this->getFinder();
51 |
52 | foreach ($finder->getIterator() as $file) {
53 | try {
54 | $abstractSyntaxTree = $phpParser->parse($file->getContents());
55 | } catch (PhpParserError $e) {
56 | throw new CodeAnalysisException(
57 | sprintf('Error parsing file `%s`', $file->getPathname()),
58 | $e->getCode(),
59 | $e
60 | );
61 | }
62 |
63 | if ($abstractSyntaxTree === null) {
64 | continue;
65 | }
66 |
67 | $astParser = new Parser();
68 | $astParser->process($abstractSyntaxTree);
69 |
70 | foreach ($astParser->getUsedNamespaces() as $namespace) {
71 | if (!$this->validator->isValidBetween($astParser->getDeclaredNamespace(), $namespace)) {
72 | $errors[] = $this->validator->getErrorMessage($astParser->getDeclaredNamespace(), $namespace);
73 | }
74 | }
75 | }
76 |
77 | return array_merge(...$errors);
78 | }
79 |
80 | /**
81 | * Adds a new validation.
82 | */
83 | public function validate(Validator $validator): self
84 | {
85 | $this->validator->addValidator($validator);
86 | return $this;
87 | }
88 |
89 | /**
90 | * Adds a source directory.
91 | */
92 | public function fromDirectory(string $directory): self
93 | {
94 | $this->directories[] = $directory;
95 | return $this;
96 | }
97 |
98 | private function getFinder(): Finder
99 | {
100 | return (new Finder())
101 | ->files()
102 | ->name('*.php')
103 | ->in($this->directories)
104 | ->sortByName();
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/Utility/ArrayUtility.php:
--------------------------------------------------------------------------------
1 | contains('MyVendor\\MyPackage\\MyComponent');
15 | * // => true
16 | * $namespace->contains('MyVendor\\MyOtherPackage\\MyComponent');
17 | * // => false
18 | */
19 | class NamespaceComperator
20 | {
21 |
22 | /** @var string[] */
23 | private array $comparison;
24 |
25 | public function __construct(string $comparison)
26 | {
27 | $this->comparison = explode('\\', trim($comparison, '\\'));
28 | }
29 |
30 | public function __toString(): string
31 | {
32 | return implode('\\', $this->comparison);
33 | }
34 |
35 | public function contains(string $namespace): bool
36 | {
37 | $parts = explode('\\', trim($namespace, '\\'));
38 |
39 | // If the comperator namespace is more specific than the compared namespace
40 | // then there is no way that the compared namespace can be inside of it.
41 | if (\count($this->comparison) > \count($parts)) {
42 | return false;
43 | }
44 |
45 | $end = min(\count($parts), \count($this->comparison));
46 | for ($i = 0; $i < $end; $i++) {
47 | if ($this->comparison[$i] !== $parts[$i]) {
48 | return false;
49 | }
50 | }
51 |
52 | return true;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Utility/NamespaceComperatorCollection.php:
--------------------------------------------------------------------------------
1 | containsAny('App\\Http\\Controllers\\MyController');
18 | * // => true
19 | *
20 | * $component->containsAny('App\\ApiV1\\Controllers\\MyController');
21 | * // => true
22 | *
23 | * $component->containsAny('App\\Utility\\PhoneNumberUtility');
24 | * // => false
25 | */
26 | class NamespaceComperatorCollection
27 | {
28 |
29 | /** @var NamespaceComperator[] */
30 | private array $comperators;
31 |
32 | /** @param string|string[] $namespaces */
33 | public function __construct($namespaces)
34 | {
35 | if (is_string($namespaces)) {
36 | $namespaces = [ $namespaces ];
37 | }
38 | $this->comperators = array_map(
39 | static function (string $namespace) {
40 | return new NamespaceComperator($namespace);
41 | },
42 | $namespaces
43 | );
44 | }
45 |
46 | public function containsAny(string $namespace): bool
47 | {
48 | foreach ($this->comperators as $comperator) {
49 | if ($comperator->contains($namespace)) {
50 | return true;
51 | }
52 | }
53 | return false;
54 | }
55 |
56 | public function __toString(): string
57 | {
58 | return '[' . implode(', ', $this->comperators) . ']';
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Validation/AbstractValidationCollection.php:
--------------------------------------------------------------------------------
1 | validators = $validators;
17 | }
18 |
19 | public function isValidBetween(string $from, string $to): bool
20 | {
21 | $this->errors = [];
22 |
23 | $valid = true;
24 | foreach ($this->getValidators() as $validator) {
25 | if (!$validator->isValidBetween($from, $to)) {
26 | foreach ($validator->getErrorMessage($from, $to) as $error) {
27 | $this->addError($from, $to, $error);
28 | }
29 | $valid = false;
30 | }
31 | }
32 |
33 | return $valid;
34 | }
35 |
36 | public function getErrorMessage(string $from, string $to): array
37 | {
38 | if (!array_key_exists($from . $to, $this->errors)) {
39 | return [];
40 | }
41 |
42 | return $this->errors[$from . $to];
43 | }
44 |
45 | /** @return Validator[] */
46 | protected function getValidators(): array
47 | {
48 | return $this->validators;
49 | }
50 |
51 | private function addError(string $from, string $to, string $message): void
52 | {
53 | if (!array_key_exists($from . $to, $this->errors)) {
54 | $this->errors[$from . $to] = [];
55 | }
56 | $this->errors[$from . $to][] = $message;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Validation/AllowInterfaces.php:
--------------------------------------------------------------------------------
1 | validator = $validator;
13 | }
14 |
15 | public function isValidBetween(string $from, string $to): bool
16 | {
17 | if (interface_exists($to)) {
18 | return true;
19 | }
20 | return $this->validator->isValidBetween($from, $to);
21 | }
22 |
23 | public function getErrorMessage(string $from, string $to): array
24 | {
25 | return $this->validator->getErrorMessage($from, $to);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Validation/ExplicitlyAllowDependency.php:
--------------------------------------------------------------------------------
1 | validator = $validator;
21 | $this->from = new NamespaceComperator($from);
22 | $this->to = new NamespaceComperatorCollection($to);
23 | }
24 |
25 | public function isValidBetween(string $from, string $to): bool
26 | {
27 | if ($this->isExplicitlyAllowed($from, $to)) {
28 | return true;
29 | }
30 |
31 | return $this->validator->isValidBetween($from, $to);
32 | }
33 |
34 | public function getErrorMessage(string $from, string $to): array
35 | {
36 | if ($this->isExplicitlyAllowed($from, $to)) {
37 | return [];
38 | }
39 |
40 | return $this->validator->getErrorMessage($from, $to);
41 | }
42 |
43 | private function isExplicitlyAllowed(string $from, string $to): bool
44 | {
45 | return $this->from->contains($from) && $this->to->containsAny($to);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Validation/ForbiddenDependency.php:
--------------------------------------------------------------------------------
1 | from = new NamespaceComperator($from);
23 | $this->to = new NamespaceComperator($to);
24 | $this->message = $message;
25 | }
26 |
27 | public function isValidBetween(string $from, string $to): bool
28 | {
29 | return !$this->from->contains($from) || !$this->to->contains($to);
30 | }
31 |
32 | public function getErrorMessage(string $from, string $to): array
33 | {
34 | $message = str_replace(
35 | [ ':from', ':to', ':violatingFrom', ':violatingTo' ],
36 | [ $this->from, $this->to, $from, $to ],
37 | $this->message
38 | );
39 | return [ $message ];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Validation/MustBeSelfContained.php:
--------------------------------------------------------------------------------
1 | namespace = new NamespaceComperatorCollection($namespace);
29 | $this->message = $message;
30 | }
31 |
32 | public function isValidBetween(string $from, string $to): bool
33 | {
34 | if (!$this->namespace->containsAny($from)) {
35 | return true;
36 | }
37 |
38 | return $this->namespace->containsAny($to);
39 | }
40 |
41 | public function getErrorMessage(string $from, string $to): array
42 | {
43 | $message = str_replace(
44 | [ ':namespace', ':violatingFrom', ':violatingTo' ],
45 | [ $this->namespace, $from, $to ],
46 | $this->message
47 | );
48 | return [ $message ];
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Validation/MustOnlyDependOn.php:
--------------------------------------------------------------------------------
1 | from = new NamespaceComperator($from);
31 | $this->to = new NamespaceComperatorCollection($to);
32 | $this->message = $message;
33 | }
34 |
35 | public function isValidBetween(string $from, string $to): bool
36 | {
37 | if (!$this->from->contains($from)) {
38 | return true;
39 | }
40 |
41 | return $this->to->containsAny($to) || $this->from->contains($to);
42 | }
43 |
44 | public function getErrorMessage(string $from, string $to): array
45 | {
46 | $message = str_replace(
47 | [ ':from', ':to', ':violatingFrom', ':violatingTo' ],
48 | [ $this->from, $this->to, $from, $to ],
49 | $this->message
50 | );
51 | return [ $message ];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Validation/MustOnlyDependOnComposerDependencies.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
28 | parent::__construct($from, $parser->getDeepRequirementNamespaces($includeDev), $message);
29 | }
30 |
31 | public function isValidBetween(string $from, string $to): bool
32 | {
33 | // Blanket assumption: All classes without a namespace (on root level) are
34 | // PHP internals and thus are always allowed.
35 | if (strpos($to, '\\') === false) {
36 | return true;
37 | }
38 |
39 | return parent::isValidBetween($from, $to);
40 | }
41 |
42 | public function getErrorMessage(string $from, string $to): array
43 | {
44 | return array_map(
45 | function (string $message): string {
46 | return str_replace(
47 | [ ':composerFile', ':lockFile' ],
48 | [ $this->parser->getComposerFilePath(), $this->parser->getLockFilePath() ],
49 | $message
50 | );
51 | },
52 | parent::getErrorMessage($from, $to)
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Validation/MustOnlyHaveAutoloadableDependencies.php:
--------------------------------------------------------------------------------
1 | message = $message;
13 | }
14 | public function isValidBetween(string $from, string $to): bool
15 | {
16 | return class_exists($to) || interface_exists($to) || trait_exists($to) || function_exists($to);
17 | }
18 |
19 | public function getErrorMessage(string $from, string $to): array
20 | {
21 | return [ str_replace([':from', ':to'], [$from, $to], $this->message) ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Validation/ValidationCollection.php:
--------------------------------------------------------------------------------
1 | validators[] = $validator;
11 | return $this;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Validation/Validator.php:
--------------------------------------------------------------------------------
1 | components([
21 | 'Component' => 'J6s\\PhpArch\\Component',
22 | 'Validation' => 'J6s\\PhpArch\\Validation',
23 | 'Exceptions' => 'J6s\\PhpArch\\Exception',
24 | 'Parser' => 'J6s\\PhpArch\\Parser',
25 | 'PHP_Core:Exception' => 'Exception'
26 | ]);
27 |
28 | $architecture->component('Validation')->mustNotDependOn('Component');
29 | $architecture->component('Exceptions')->mustOnlyDependOn('PHP_Core:Exception');
30 | $architecture->component('Parser')
31 | ->mustNotDependOn('Component')
32 | ->andMustNotDependOn('Validation');
33 |
34 | $utility = new ExplicitlyAllowDependency(
35 | new MustBeSelfContained('J6s\\PhpArch\\Utility'),
36 | 'J6s\\PhpArch\\Utility',
37 | 'Safe\\'
38 | );
39 |
40 | (new PhpArch())
41 | ->fromDirectory(__DIR__ . '/../src')
42 | ->validate($utility)
43 | ->validate($architecture)
44 | ->assertHasNoErrors();
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Component/ArchitectureTest.php:
--------------------------------------------------------------------------------
1 | component('test')->identifiedByNamespace(static::TEST_NAMESPACE)
23 | ->mustNotDependOn('forbidden')->identifiedByNamespace(static::FORBIDDEN_NAMESPACE);
24 |
25 | $this->assertTrue($architecture->isValidBetween(TestClass::class, AllowedDependency::class));
26 | $this->assertFalse($architecture->isValidBetween(TestClass::class, ForbiddenDependency::class));
27 | $this->assertTrue($architecture->isValidBetween(TestClass::class, InsideDependency::class));
28 | $this->assertTrue($architecture->isValidBetween(TestClass::class, OutsideDependency::class));
29 | }
30 |
31 | public function testProvidesSpeakingApiForCreatingInverseForbiddenDependencyConstraints(): void
32 | {
33 | $architecture = (new Architecture())
34 | ->component('forbidden')->identifiedByNamespace(static::FORBIDDEN_NAMESPACE)
35 | ->mustNotBeDependedOnBy('test')->identifiedByNamespace(static::TEST_NAMESPACE);
36 |
37 | $this->assertTrue($architecture->isValidBetween(TestClass::class, AllowedDependency::class));
38 | $this->assertFalse($architecture->isValidBetween(TestClass::class, ForbiddenDependency::class));
39 | $this->assertTrue($architecture->isValidBetween(TestClass::class, InsideDependency::class));
40 | $this->assertTrue($architecture->isValidBetween(TestClass::class, OutsideDependency::class));
41 | }
42 |
43 | public function testProvidesSpeakingApiForCreatingOnlyAllowedComponentConstraint(): void
44 | {
45 | $architecture = (new Architecture())
46 | ->component('test')->identifiedByNamespace(static::TEST_NAMESPACE)
47 | ->mustOnlyDependOn('allowed')->identifiedByNamespace(static::ALLOWED_NAMESPACE);
48 |
49 | $this->assertTrue($architecture->isValidBetween(TestClass::class, AllowedDependency::class));
50 | $this->assertFalse($architecture->isValidBetween(TestClass::class, ForbiddenDependency::class));
51 | $this->assertTrue($architecture->isValidBetween(TestClass::class, InsideDependency::class));
52 | $this->assertFalse($architecture->isValidBetween(TestClass::class, OutsideDependency::class));
53 | }
54 |
55 | public function testAllowsChainingDependencyConstraints(): void
56 | {
57 | $architecture = (new Architecture())
58 | ->component('test')->identifiedByNamespace(static::TEST_NAMESPACE)
59 | ->mustOnlyDependOn('allowed')->identifiedByNamespace(static::ALLOWED_NAMESPACE)
60 | ->andMustNotBeDependedOnBy('forbidden')->identifiedByNamespace(static::FORBIDDEN_NAMESPACE);
61 |
62 | $this->assertTrue($architecture->isValidBetween(TestClass::class, AllowedDependency::class));
63 | $this->assertFalse($architecture->isValidBetween(TestClass::class, ForbiddenDependency::class));
64 | $this->assertTrue($architecture->isValidBetween(TestClass::class, InsideDependency::class));
65 | $this->assertFalse($architecture->isValidBetween(TestClass::class, OutsideDependency::class));
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/tests/Component/ComponentTest.php:
--------------------------------------------------------------------------------
1 | addNamespace(static::TEST_NAMESPACE);
26 | $forbidden->addNamespace(static::FORBIDDEN_NAMESPACE);
27 |
28 | $test->mustNotDependOn($forbidden);
29 |
30 | $this->assertTrue($test->isValidBetween(TestClass::class, AllowedDependency::class));
31 | $this->assertFalse($test->isValidBetween(TestClass::class, ForbiddenDependency::class));
32 | $this->assertTrue($test->isValidBetween(TestClass::class, InsideDependency::class));
33 | $this->assertTrue($test->isValidBetween(TestClass::class, OutsideDependency::class));
34 | }
35 |
36 | public function testComponentCanBeDefinedToOnlyDependOnAnotherComponent(): void
37 | {
38 | $test = new Component('test');
39 | $allowed = new Component('allowed');
40 |
41 | $test->addNamespace(static::TEST_NAMESPACE);
42 | $allowed->addNamespace(static::ALLOWED_NAMESPACE);
43 |
44 | $test->mustOnlyDependOn($allowed);
45 |
46 | $this->assertTrue($test->isValidBetween(TestClass::class, AllowedDependency::class));
47 | $this->assertFalse($test->isValidBetween(TestClass::class, ForbiddenDependency::class));
48 | $this->assertTrue($test->isValidBetween(TestClass::class, InsideDependency::class));
49 | $this->assertFalse($test->isValidBetween(TestClass::class, OutsideDependency::class));
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/tests/Component/Example/Allowed/AllowedDependency.php:
--------------------------------------------------------------------------------
1 | subject = new ComposerFileParser(__DIR__ . '/Mock/composer.json');
18 | }
19 |
20 | public function testExtractsName(): void
21 | {
22 | $this->assertEquals(
23 | 'j6s/phparch-mock',
24 | $this->subject->getName()
25 | );
26 | }
27 |
28 | public function testExtractsNamespaces(): void
29 | {
30 | $this->assertEquals(
31 | [ 'J6s\\PhpArchMock' ],
32 | $this->subject->getNamespaces(false)
33 | );
34 | $this->assertEquals(
35 | [ 'J6s\\PhpArchMock', 'J6s\\PhpArchMock\\Tests' ],
36 | $this->subject->getNamespaces(true)
37 | );
38 | }
39 |
40 | public function testShouldExtractDependencies(): void
41 | {
42 | $this->assertEquals(
43 | [ 'thecodingmachine/safe' ],
44 | $this->subject->getDirectDependencies(false)
45 | );
46 | $this->assertEquals(
47 | [ 'thecodingmachine/safe', 'phpunit/phpunit' ],
48 | $this->subject->getDirectDependencies(true)
49 | );
50 | }
51 |
52 | public function testExtractsNamespacesForPackageName()
53 | {
54 | $this->assertContains(
55 | 'Safe\\',
56 | $this->subject->autoloadableNamespacesForRequirements([ 'thecodingmachine/safe' ], false)
57 | );
58 | }
59 |
60 | public function testDeepRequirementNamespacesContainsDepenenciesOfDependencies()
61 | {
62 | $namespaces = $this->subject->getDeepRequirementNamespaces(true);
63 |
64 | // phpunit/phpunit depends on doctrine/instantiator
65 | $this->assertContains('Doctrine\\Instantiator\\', $namespaces);
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/tests/Composer/Mock/.gitignore:
--------------------------------------------------------------------------------
1 | !composer.lock
2 |
--------------------------------------------------------------------------------
/tests/Composer/Mock/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "j6s/phparch-mock",
3 | "require": {
4 | "thecodingmachine/safe": "^0.1.15"
5 | },
6 | "require-dev": {
7 | "phpunit/phpunit": "^8.1"
8 | },
9 | "autoload": {
10 | "psr-4": {
11 | "J6s\\PhpArchMock": "FooBar"
12 | }
13 | },
14 | "autoload-dev": {
15 | "psr-4": {
16 | "J6s\\PhpArchMock\\Tests": "FooBar/Tests"
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/Composer/Mock/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "3acfc4806d409e6ad48f1df9621b6120",
8 | "packages": [
9 | {
10 | "name": "thecodingmachine/safe",
11 | "version": "v0.1.15",
12 | "source": {
13 | "type": "git",
14 | "url": "https://github.com/thecodingmachine/safe.git",
15 | "reference": "9a4dbc54e397e0bfb152f4b38f8a03040a1e9e3e"
16 | },
17 | "dist": {
18 | "type": "zip",
19 | "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/9a4dbc54e397e0bfb152f4b38f8a03040a1e9e3e",
20 | "reference": "9a4dbc54e397e0bfb152f4b38f8a03040a1e9e3e",
21 | "shasum": ""
22 | },
23 | "require": {
24 | "php": ">=7.1"
25 | },
26 | "require-dev": {
27 | "phpstan/phpstan": "^0.10.3",
28 | "squizlabs/php_codesniffer": "^3.2",
29 | "thecodingmachine/phpstan-strict-rules": "^0.10.3"
30 | },
31 | "type": "library",
32 | "extra": {
33 | "branch-alias": {
34 | "dev-master": "0.1-dev"
35 | }
36 | },
37 | "autoload": {
38 | "psr-4": {
39 | "Safe\\": [
40 | "lib/",
41 | "generated/"
42 | ]
43 | },
44 | "files": [
45 | "generated/apache.php",
46 | "generated/apc.php",
47 | "generated/apcu.php",
48 | "generated/array.php",
49 | "generated/bzip2.php",
50 | "generated/classobj.php",
51 | "generated/com.php",
52 | "generated/cubrid.php",
53 | "generated/curl.php",
54 | "generated/datetime.php",
55 | "generated/dir.php",
56 | "generated/eio.php",
57 | "generated/errorfunc.php",
58 | "generated/exec.php",
59 | "generated/fileinfo.php",
60 | "generated/filesystem.php",
61 | "generated/filter.php",
62 | "generated/fpm.php",
63 | "generated/ftp.php",
64 | "generated/funchand.php",
65 | "generated/gmp.php",
66 | "generated/gnupg.php",
67 | "generated/hash.php",
68 | "generated/ibase.php",
69 | "generated/ibmDb2.php",
70 | "generated/iconv.php",
71 | "generated/image.php",
72 | "generated/imap.php",
73 | "generated/info.php",
74 | "generated/ingres-ii.php",
75 | "generated/inotify.php",
76 | "generated/json.php",
77 | "generated/ldap.php",
78 | "generated/libevent.php",
79 | "generated/libxml.php",
80 | "generated/lzf.php",
81 | "generated/mailparse.php",
82 | "generated/mbstring.php",
83 | "generated/misc.php",
84 | "generated/msql.php",
85 | "generated/mssql.php",
86 | "generated/mysql.php",
87 | "generated/mysqli.php",
88 | "generated/mysqlndMs.php",
89 | "generated/mysqlndQc.php",
90 | "generated/network.php",
91 | "generated/oci8.php",
92 | "generated/opcache.php",
93 | "generated/openssl.php",
94 | "generated/outcontrol.php",
95 | "generated/password.php",
96 | "generated/pcntl.php",
97 | "generated/pcre.php",
98 | "generated/pdf.php",
99 | "generated/pgsql.php",
100 | "generated/posix.php",
101 | "generated/ps.php",
102 | "generated/pspell.php",
103 | "generated/readline.php",
104 | "generated/rrd.php",
105 | "generated/sem.php",
106 | "generated/session.php",
107 | "generated/shmop.php",
108 | "generated/simplexml.php",
109 | "generated/sockets.php",
110 | "generated/sodium.php",
111 | "generated/solr.php",
112 | "generated/spl.php",
113 | "generated/sqlsrv.php",
114 | "generated/ssdeep.php",
115 | "generated/ssh2.php",
116 | "generated/stats.php",
117 | "generated/stream.php",
118 | "generated/strings.php",
119 | "generated/swoole.php",
120 | "generated/uodbc.php",
121 | "generated/uopz.php",
122 | "generated/url.php",
123 | "generated/var.php",
124 | "generated/xdiff.php",
125 | "generated/xml.php",
126 | "generated/xmlrpc.php",
127 | "generated/yaml.php",
128 | "generated/yaz.php",
129 | "generated/zip.php",
130 | "generated/zlib.php",
131 | "lib/special_cases.php"
132 | ]
133 | },
134 | "notification-url": "https://packagist.org/downloads/",
135 | "license": [
136 | "MIT"
137 | ],
138 | "description": "PHP core functions that throw exceptions instead of returning FALSE on error",
139 | "time": "2019-04-17T16:09:50+00:00"
140 | }
141 | ],
142 | "packages-dev": [
143 | {
144 | "name": "doctrine/instantiator",
145 | "version": "1.2.0",
146 | "source": {
147 | "type": "git",
148 | "url": "https://github.com/doctrine/instantiator.git",
149 | "reference": "a2c590166b2133a4633738648b6b064edae0814a"
150 | },
151 | "dist": {
152 | "type": "zip",
153 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a",
154 | "reference": "a2c590166b2133a4633738648b6b064edae0814a",
155 | "shasum": ""
156 | },
157 | "require": {
158 | "php": "^7.1"
159 | },
160 | "require-dev": {
161 | "doctrine/coding-standard": "^6.0",
162 | "ext-pdo": "*",
163 | "ext-phar": "*",
164 | "phpbench/phpbench": "^0.13",
165 | "phpstan/phpstan-phpunit": "^0.11",
166 | "phpstan/phpstan-shim": "^0.11",
167 | "phpunit/phpunit": "^7.0"
168 | },
169 | "type": "library",
170 | "extra": {
171 | "branch-alias": {
172 | "dev-master": "1.2.x-dev"
173 | }
174 | },
175 | "autoload": {
176 | "psr-4": {
177 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
178 | }
179 | },
180 | "notification-url": "https://packagist.org/downloads/",
181 | "license": [
182 | "MIT"
183 | ],
184 | "authors": [
185 | {
186 | "name": "Marco Pivetta",
187 | "email": "ocramius@gmail.com",
188 | "homepage": "http://ocramius.github.com/"
189 | }
190 | ],
191 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
192 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
193 | "keywords": [
194 | "constructor",
195 | "instantiate"
196 | ],
197 | "time": "2019-03-17T17:37:11+00:00"
198 | },
199 | {
200 | "name": "myclabs/deep-copy",
201 | "version": "1.9.1",
202 | "source": {
203 | "type": "git",
204 | "url": "https://github.com/myclabs/DeepCopy.git",
205 | "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72"
206 | },
207 | "dist": {
208 | "type": "zip",
209 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72",
210 | "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72",
211 | "shasum": ""
212 | },
213 | "require": {
214 | "php": "^7.1"
215 | },
216 | "replace": {
217 | "myclabs/deep-copy": "self.version"
218 | },
219 | "require-dev": {
220 | "doctrine/collections": "^1.0",
221 | "doctrine/common": "^2.6",
222 | "phpunit/phpunit": "^7.1"
223 | },
224 | "type": "library",
225 | "autoload": {
226 | "psr-4": {
227 | "DeepCopy\\": "src/DeepCopy/"
228 | },
229 | "files": [
230 | "src/DeepCopy/deep_copy.php"
231 | ]
232 | },
233 | "notification-url": "https://packagist.org/downloads/",
234 | "license": [
235 | "MIT"
236 | ],
237 | "description": "Create deep copies (clones) of your objects",
238 | "keywords": [
239 | "clone",
240 | "copy",
241 | "duplicate",
242 | "object",
243 | "object graph"
244 | ],
245 | "time": "2019-04-07T13:18:21+00:00"
246 | },
247 | {
248 | "name": "phar-io/manifest",
249 | "version": "1.0.3",
250 | "source": {
251 | "type": "git",
252 | "url": "https://github.com/phar-io/manifest.git",
253 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
254 | },
255 | "dist": {
256 | "type": "zip",
257 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
258 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
259 | "shasum": ""
260 | },
261 | "require": {
262 | "ext-dom": "*",
263 | "ext-phar": "*",
264 | "phar-io/version": "^2.0",
265 | "php": "^5.6 || ^7.0"
266 | },
267 | "type": "library",
268 | "extra": {
269 | "branch-alias": {
270 | "dev-master": "1.0.x-dev"
271 | }
272 | },
273 | "autoload": {
274 | "classmap": [
275 | "src/"
276 | ]
277 | },
278 | "notification-url": "https://packagist.org/downloads/",
279 | "license": [
280 | "BSD-3-Clause"
281 | ],
282 | "authors": [
283 | {
284 | "name": "Arne Blankerts",
285 | "email": "arne@blankerts.de",
286 | "role": "Developer"
287 | },
288 | {
289 | "name": "Sebastian Heuer",
290 | "email": "sebastian@phpeople.de",
291 | "role": "Developer"
292 | },
293 | {
294 | "name": "Sebastian Bergmann",
295 | "email": "sebastian@phpunit.de",
296 | "role": "Developer"
297 | }
298 | ],
299 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
300 | "time": "2018-07-08T19:23:20+00:00"
301 | },
302 | {
303 | "name": "phar-io/version",
304 | "version": "2.0.1",
305 | "source": {
306 | "type": "git",
307 | "url": "https://github.com/phar-io/version.git",
308 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
309 | },
310 | "dist": {
311 | "type": "zip",
312 | "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
313 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
314 | "shasum": ""
315 | },
316 | "require": {
317 | "php": "^5.6 || ^7.0"
318 | },
319 | "type": "library",
320 | "autoload": {
321 | "classmap": [
322 | "src/"
323 | ]
324 | },
325 | "notification-url": "https://packagist.org/downloads/",
326 | "license": [
327 | "BSD-3-Clause"
328 | ],
329 | "authors": [
330 | {
331 | "name": "Arne Blankerts",
332 | "email": "arne@blankerts.de",
333 | "role": "Developer"
334 | },
335 | {
336 | "name": "Sebastian Heuer",
337 | "email": "sebastian@phpeople.de",
338 | "role": "Developer"
339 | },
340 | {
341 | "name": "Sebastian Bergmann",
342 | "email": "sebastian@phpunit.de",
343 | "role": "Developer"
344 | }
345 | ],
346 | "description": "Library for handling version information and constraints",
347 | "time": "2018-07-08T19:19:57+00:00"
348 | },
349 | {
350 | "name": "phpdocumentor/reflection-common",
351 | "version": "1.0.1",
352 | "source": {
353 | "type": "git",
354 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
355 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
356 | },
357 | "dist": {
358 | "type": "zip",
359 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
360 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
361 | "shasum": ""
362 | },
363 | "require": {
364 | "php": ">=5.5"
365 | },
366 | "require-dev": {
367 | "phpunit/phpunit": "^4.6"
368 | },
369 | "type": "library",
370 | "extra": {
371 | "branch-alias": {
372 | "dev-master": "1.0.x-dev"
373 | }
374 | },
375 | "autoload": {
376 | "psr-4": {
377 | "phpDocumentor\\Reflection\\": [
378 | "src"
379 | ]
380 | }
381 | },
382 | "notification-url": "https://packagist.org/downloads/",
383 | "license": [
384 | "MIT"
385 | ],
386 | "authors": [
387 | {
388 | "name": "Jaap van Otterdijk",
389 | "email": "opensource@ijaap.nl"
390 | }
391 | ],
392 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
393 | "homepage": "http://www.phpdoc.org",
394 | "keywords": [
395 | "FQSEN",
396 | "phpDocumentor",
397 | "phpdoc",
398 | "reflection",
399 | "static analysis"
400 | ],
401 | "time": "2017-09-11T18:02:19+00:00"
402 | },
403 | {
404 | "name": "phpdocumentor/reflection-docblock",
405 | "version": "4.3.1",
406 | "source": {
407 | "type": "git",
408 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
409 | "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c"
410 | },
411 | "dist": {
412 | "type": "zip",
413 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c",
414 | "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c",
415 | "shasum": ""
416 | },
417 | "require": {
418 | "php": "^7.0",
419 | "phpdocumentor/reflection-common": "^1.0.0",
420 | "phpdocumentor/type-resolver": "^0.4.0",
421 | "webmozart/assert": "^1.0"
422 | },
423 | "require-dev": {
424 | "doctrine/instantiator": "~1.0.5",
425 | "mockery/mockery": "^1.0",
426 | "phpunit/phpunit": "^6.4"
427 | },
428 | "type": "library",
429 | "extra": {
430 | "branch-alias": {
431 | "dev-master": "4.x-dev"
432 | }
433 | },
434 | "autoload": {
435 | "psr-4": {
436 | "phpDocumentor\\Reflection\\": [
437 | "src/"
438 | ]
439 | }
440 | },
441 | "notification-url": "https://packagist.org/downloads/",
442 | "license": [
443 | "MIT"
444 | ],
445 | "authors": [
446 | {
447 | "name": "Mike van Riel",
448 | "email": "me@mikevanriel.com"
449 | }
450 | ],
451 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
452 | "time": "2019-04-30T17:48:53+00:00"
453 | },
454 | {
455 | "name": "phpdocumentor/type-resolver",
456 | "version": "0.4.0",
457 | "source": {
458 | "type": "git",
459 | "url": "https://github.com/phpDocumentor/TypeResolver.git",
460 | "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
461 | },
462 | "dist": {
463 | "type": "zip",
464 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
465 | "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
466 | "shasum": ""
467 | },
468 | "require": {
469 | "php": "^5.5 || ^7.0",
470 | "phpdocumentor/reflection-common": "^1.0"
471 | },
472 | "require-dev": {
473 | "mockery/mockery": "^0.9.4",
474 | "phpunit/phpunit": "^5.2||^4.8.24"
475 | },
476 | "type": "library",
477 | "extra": {
478 | "branch-alias": {
479 | "dev-master": "1.0.x-dev"
480 | }
481 | },
482 | "autoload": {
483 | "psr-4": {
484 | "phpDocumentor\\Reflection\\": [
485 | "src/"
486 | ]
487 | }
488 | },
489 | "notification-url": "https://packagist.org/downloads/",
490 | "license": [
491 | "MIT"
492 | ],
493 | "authors": [
494 | {
495 | "name": "Mike van Riel",
496 | "email": "me@mikevanriel.com"
497 | }
498 | ],
499 | "time": "2017-07-14T14:27:02+00:00"
500 | },
501 | {
502 | "name": "phpspec/prophecy",
503 | "version": "1.8.0",
504 | "source": {
505 | "type": "git",
506 | "url": "https://github.com/phpspec/prophecy.git",
507 | "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
508 | },
509 | "dist": {
510 | "type": "zip",
511 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
512 | "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
513 | "shasum": ""
514 | },
515 | "require": {
516 | "doctrine/instantiator": "^1.0.2",
517 | "php": "^5.3|^7.0",
518 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
519 | "sebastian/comparator": "^1.1|^2.0|^3.0",
520 | "sebastian/recursion-context": "^1.0|^2.0|^3.0"
521 | },
522 | "require-dev": {
523 | "phpspec/phpspec": "^2.5|^3.2",
524 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
525 | },
526 | "type": "library",
527 | "extra": {
528 | "branch-alias": {
529 | "dev-master": "1.8.x-dev"
530 | }
531 | },
532 | "autoload": {
533 | "psr-0": {
534 | "Prophecy\\": "src/"
535 | }
536 | },
537 | "notification-url": "https://packagist.org/downloads/",
538 | "license": [
539 | "MIT"
540 | ],
541 | "authors": [
542 | {
543 | "name": "Konstantin Kudryashov",
544 | "email": "ever.zet@gmail.com",
545 | "homepage": "http://everzet.com"
546 | },
547 | {
548 | "name": "Marcello Duarte",
549 | "email": "marcello.duarte@gmail.com"
550 | }
551 | ],
552 | "description": "Highly opinionated mocking framework for PHP 5.3+",
553 | "homepage": "https://github.com/phpspec/prophecy",
554 | "keywords": [
555 | "Double",
556 | "Dummy",
557 | "fake",
558 | "mock",
559 | "spy",
560 | "stub"
561 | ],
562 | "time": "2018-08-05T17:53:17+00:00"
563 | },
564 | {
565 | "name": "phpunit/php-code-coverage",
566 | "version": "7.0.3",
567 | "source": {
568 | "type": "git",
569 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
570 | "reference": "0317a769a81845c390e19684d9ba25d7f6aa4707"
571 | },
572 | "dist": {
573 | "type": "zip",
574 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0317a769a81845c390e19684d9ba25d7f6aa4707",
575 | "reference": "0317a769a81845c390e19684d9ba25d7f6aa4707",
576 | "shasum": ""
577 | },
578 | "require": {
579 | "ext-dom": "*",
580 | "ext-xmlwriter": "*",
581 | "php": "^7.2",
582 | "phpunit/php-file-iterator": "^2.0.2",
583 | "phpunit/php-text-template": "^1.2.1",
584 | "phpunit/php-token-stream": "^3.0.1",
585 | "sebastian/code-unit-reverse-lookup": "^1.0.1",
586 | "sebastian/environment": "^4.1",
587 | "sebastian/version": "^2.0.1",
588 | "theseer/tokenizer": "^1.1"
589 | },
590 | "require-dev": {
591 | "phpunit/phpunit": "^8.0"
592 | },
593 | "suggest": {
594 | "ext-xdebug": "^2.6.1"
595 | },
596 | "type": "library",
597 | "extra": {
598 | "branch-alias": {
599 | "dev-master": "7.0-dev"
600 | }
601 | },
602 | "autoload": {
603 | "classmap": [
604 | "src/"
605 | ]
606 | },
607 | "notification-url": "https://packagist.org/downloads/",
608 | "license": [
609 | "BSD-3-Clause"
610 | ],
611 | "authors": [
612 | {
613 | "name": "Sebastian Bergmann",
614 | "email": "sebastian@phpunit.de",
615 | "role": "lead"
616 | }
617 | ],
618 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
619 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
620 | "keywords": [
621 | "coverage",
622 | "testing",
623 | "xunit"
624 | ],
625 | "time": "2019-02-26T07:38:26+00:00"
626 | },
627 | {
628 | "name": "phpunit/php-file-iterator",
629 | "version": "2.0.2",
630 | "source": {
631 | "type": "git",
632 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
633 | "reference": "050bedf145a257b1ff02746c31894800e5122946"
634 | },
635 | "dist": {
636 | "type": "zip",
637 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
638 | "reference": "050bedf145a257b1ff02746c31894800e5122946",
639 | "shasum": ""
640 | },
641 | "require": {
642 | "php": "^7.1"
643 | },
644 | "require-dev": {
645 | "phpunit/phpunit": "^7.1"
646 | },
647 | "type": "library",
648 | "extra": {
649 | "branch-alias": {
650 | "dev-master": "2.0.x-dev"
651 | }
652 | },
653 | "autoload": {
654 | "classmap": [
655 | "src/"
656 | ]
657 | },
658 | "notification-url": "https://packagist.org/downloads/",
659 | "license": [
660 | "BSD-3-Clause"
661 | ],
662 | "authors": [
663 | {
664 | "name": "Sebastian Bergmann",
665 | "email": "sebastian@phpunit.de",
666 | "role": "lead"
667 | }
668 | ],
669 | "description": "FilterIterator implementation that filters files based on a list of suffixes.",
670 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
671 | "keywords": [
672 | "filesystem",
673 | "iterator"
674 | ],
675 | "time": "2018-09-13T20:33:42+00:00"
676 | },
677 | {
678 | "name": "phpunit/php-text-template",
679 | "version": "1.2.1",
680 | "source": {
681 | "type": "git",
682 | "url": "https://github.com/sebastianbergmann/php-text-template.git",
683 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
684 | },
685 | "dist": {
686 | "type": "zip",
687 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
688 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
689 | "shasum": ""
690 | },
691 | "require": {
692 | "php": ">=5.3.3"
693 | },
694 | "type": "library",
695 | "autoload": {
696 | "classmap": [
697 | "src/"
698 | ]
699 | },
700 | "notification-url": "https://packagist.org/downloads/",
701 | "license": [
702 | "BSD-3-Clause"
703 | ],
704 | "authors": [
705 | {
706 | "name": "Sebastian Bergmann",
707 | "email": "sebastian@phpunit.de",
708 | "role": "lead"
709 | }
710 | ],
711 | "description": "Simple template engine.",
712 | "homepage": "https://github.com/sebastianbergmann/php-text-template/",
713 | "keywords": [
714 | "template"
715 | ],
716 | "time": "2015-06-21T13:50:34+00:00"
717 | },
718 | {
719 | "name": "phpunit/php-timer",
720 | "version": "2.1.1",
721 | "source": {
722 | "type": "git",
723 | "url": "https://github.com/sebastianbergmann/php-timer.git",
724 | "reference": "8b389aebe1b8b0578430bda0c7c95a829608e059"
725 | },
726 | "dist": {
727 | "type": "zip",
728 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b389aebe1b8b0578430bda0c7c95a829608e059",
729 | "reference": "8b389aebe1b8b0578430bda0c7c95a829608e059",
730 | "shasum": ""
731 | },
732 | "require": {
733 | "php": "^7.1"
734 | },
735 | "require-dev": {
736 | "phpunit/phpunit": "^7.0"
737 | },
738 | "type": "library",
739 | "extra": {
740 | "branch-alias": {
741 | "dev-master": "2.1-dev"
742 | }
743 | },
744 | "autoload": {
745 | "classmap": [
746 | "src/"
747 | ]
748 | },
749 | "notification-url": "https://packagist.org/downloads/",
750 | "license": [
751 | "BSD-3-Clause"
752 | ],
753 | "authors": [
754 | {
755 | "name": "Sebastian Bergmann",
756 | "email": "sebastian@phpunit.de",
757 | "role": "lead"
758 | }
759 | ],
760 | "description": "Utility class for timing",
761 | "homepage": "https://github.com/sebastianbergmann/php-timer/",
762 | "keywords": [
763 | "timer"
764 | ],
765 | "time": "2019-02-20T10:12:59+00:00"
766 | },
767 | {
768 | "name": "phpunit/php-token-stream",
769 | "version": "3.0.1",
770 | "source": {
771 | "type": "git",
772 | "url": "https://github.com/sebastianbergmann/php-token-stream.git",
773 | "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18"
774 | },
775 | "dist": {
776 | "type": "zip",
777 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c99e3be9d3e85f60646f152f9002d46ed7770d18",
778 | "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18",
779 | "shasum": ""
780 | },
781 | "require": {
782 | "ext-tokenizer": "*",
783 | "php": "^7.1"
784 | },
785 | "require-dev": {
786 | "phpunit/phpunit": "^7.0"
787 | },
788 | "type": "library",
789 | "extra": {
790 | "branch-alias": {
791 | "dev-master": "3.0-dev"
792 | }
793 | },
794 | "autoload": {
795 | "classmap": [
796 | "src/"
797 | ]
798 | },
799 | "notification-url": "https://packagist.org/downloads/",
800 | "license": [
801 | "BSD-3-Clause"
802 | ],
803 | "authors": [
804 | {
805 | "name": "Sebastian Bergmann",
806 | "email": "sebastian@phpunit.de"
807 | }
808 | ],
809 | "description": "Wrapper around PHP's tokenizer extension.",
810 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
811 | "keywords": [
812 | "tokenizer"
813 | ],
814 | "time": "2018-10-30T05:52:18+00:00"
815 | },
816 | {
817 | "name": "phpunit/phpunit",
818 | "version": "8.1.5",
819 | "source": {
820 | "type": "git",
821 | "url": "https://github.com/sebastianbergmann/phpunit.git",
822 | "reference": "01392d4b5878aa617e8d9bc7a529e5febc8fe956"
823 | },
824 | "dist": {
825 | "type": "zip",
826 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/01392d4b5878aa617e8d9bc7a529e5febc8fe956",
827 | "reference": "01392d4b5878aa617e8d9bc7a529e5febc8fe956",
828 | "shasum": ""
829 | },
830 | "require": {
831 | "doctrine/instantiator": "^1.1",
832 | "ext-dom": "*",
833 | "ext-json": "*",
834 | "ext-libxml": "*",
835 | "ext-mbstring": "*",
836 | "ext-xml": "*",
837 | "ext-xmlwriter": "*",
838 | "myclabs/deep-copy": "^1.7",
839 | "phar-io/manifest": "^1.0.2",
840 | "phar-io/version": "^2.0",
841 | "php": "^7.2",
842 | "phpspec/prophecy": "^1.7",
843 | "phpunit/php-code-coverage": "^7.0",
844 | "phpunit/php-file-iterator": "^2.0.1",
845 | "phpunit/php-text-template": "^1.2.1",
846 | "phpunit/php-timer": "^2.1",
847 | "sebastian/comparator": "^3.0",
848 | "sebastian/diff": "^3.0",
849 | "sebastian/environment": "^4.1",
850 | "sebastian/exporter": "^3.1",
851 | "sebastian/global-state": "^3.0",
852 | "sebastian/object-enumerator": "^3.0.3",
853 | "sebastian/resource-operations": "^2.0",
854 | "sebastian/version": "^2.0.1"
855 | },
856 | "require-dev": {
857 | "ext-pdo": "*"
858 | },
859 | "suggest": {
860 | "ext-soap": "*",
861 | "ext-xdebug": "*",
862 | "phpunit/php-invoker": "^2.0"
863 | },
864 | "bin": [
865 | "phpunit"
866 | ],
867 | "type": "library",
868 | "extra": {
869 | "branch-alias": {
870 | "dev-master": "8.1-dev"
871 | }
872 | },
873 | "autoload": {
874 | "classmap": [
875 | "src/"
876 | ]
877 | },
878 | "notification-url": "https://packagist.org/downloads/",
879 | "license": [
880 | "BSD-3-Clause"
881 | ],
882 | "authors": [
883 | {
884 | "name": "Sebastian Bergmann",
885 | "email": "sebastian@phpunit.de",
886 | "role": "lead"
887 | }
888 | ],
889 | "description": "The PHP Unit Testing framework.",
890 | "homepage": "https://phpunit.de/",
891 | "keywords": [
892 | "phpunit",
893 | "testing",
894 | "xunit"
895 | ],
896 | "time": "2019-05-14T04:57:31+00:00"
897 | },
898 | {
899 | "name": "sebastian/code-unit-reverse-lookup",
900 | "version": "1.0.1",
901 | "source": {
902 | "type": "git",
903 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
904 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18"
905 | },
906 | "dist": {
907 | "type": "zip",
908 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
909 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
910 | "shasum": ""
911 | },
912 | "require": {
913 | "php": "^5.6 || ^7.0"
914 | },
915 | "require-dev": {
916 | "phpunit/phpunit": "^5.7 || ^6.0"
917 | },
918 | "type": "library",
919 | "extra": {
920 | "branch-alias": {
921 | "dev-master": "1.0.x-dev"
922 | }
923 | },
924 | "autoload": {
925 | "classmap": [
926 | "src/"
927 | ]
928 | },
929 | "notification-url": "https://packagist.org/downloads/",
930 | "license": [
931 | "BSD-3-Clause"
932 | ],
933 | "authors": [
934 | {
935 | "name": "Sebastian Bergmann",
936 | "email": "sebastian@phpunit.de"
937 | }
938 | ],
939 | "description": "Looks up which function or method a line of code belongs to",
940 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
941 | "time": "2017-03-04T06:30:41+00:00"
942 | },
943 | {
944 | "name": "sebastian/comparator",
945 | "version": "3.0.2",
946 | "source": {
947 | "type": "git",
948 | "url": "https://github.com/sebastianbergmann/comparator.git",
949 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
950 | },
951 | "dist": {
952 | "type": "zip",
953 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
954 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
955 | "shasum": ""
956 | },
957 | "require": {
958 | "php": "^7.1",
959 | "sebastian/diff": "^3.0",
960 | "sebastian/exporter": "^3.1"
961 | },
962 | "require-dev": {
963 | "phpunit/phpunit": "^7.1"
964 | },
965 | "type": "library",
966 | "extra": {
967 | "branch-alias": {
968 | "dev-master": "3.0-dev"
969 | }
970 | },
971 | "autoload": {
972 | "classmap": [
973 | "src/"
974 | ]
975 | },
976 | "notification-url": "https://packagist.org/downloads/",
977 | "license": [
978 | "BSD-3-Clause"
979 | ],
980 | "authors": [
981 | {
982 | "name": "Jeff Welch",
983 | "email": "whatthejeff@gmail.com"
984 | },
985 | {
986 | "name": "Volker Dusch",
987 | "email": "github@wallbash.com"
988 | },
989 | {
990 | "name": "Bernhard Schussek",
991 | "email": "bschussek@2bepublished.at"
992 | },
993 | {
994 | "name": "Sebastian Bergmann",
995 | "email": "sebastian@phpunit.de"
996 | }
997 | ],
998 | "description": "Provides the functionality to compare PHP values for equality",
999 | "homepage": "https://github.com/sebastianbergmann/comparator",
1000 | "keywords": [
1001 | "comparator",
1002 | "compare",
1003 | "equality"
1004 | ],
1005 | "time": "2018-07-12T15:12:46+00:00"
1006 | },
1007 | {
1008 | "name": "sebastian/diff",
1009 | "version": "3.0.2",
1010 | "source": {
1011 | "type": "git",
1012 | "url": "https://github.com/sebastianbergmann/diff.git",
1013 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
1014 | },
1015 | "dist": {
1016 | "type": "zip",
1017 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
1018 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
1019 | "shasum": ""
1020 | },
1021 | "require": {
1022 | "php": "^7.1"
1023 | },
1024 | "require-dev": {
1025 | "phpunit/phpunit": "^7.5 || ^8.0",
1026 | "symfony/process": "^2 || ^3.3 || ^4"
1027 | },
1028 | "type": "library",
1029 | "extra": {
1030 | "branch-alias": {
1031 | "dev-master": "3.0-dev"
1032 | }
1033 | },
1034 | "autoload": {
1035 | "classmap": [
1036 | "src/"
1037 | ]
1038 | },
1039 | "notification-url": "https://packagist.org/downloads/",
1040 | "license": [
1041 | "BSD-3-Clause"
1042 | ],
1043 | "authors": [
1044 | {
1045 | "name": "Kore Nordmann",
1046 | "email": "mail@kore-nordmann.de"
1047 | },
1048 | {
1049 | "name": "Sebastian Bergmann",
1050 | "email": "sebastian@phpunit.de"
1051 | }
1052 | ],
1053 | "description": "Diff implementation",
1054 | "homepage": "https://github.com/sebastianbergmann/diff",
1055 | "keywords": [
1056 | "diff",
1057 | "udiff",
1058 | "unidiff",
1059 | "unified diff"
1060 | ],
1061 | "time": "2019-02-04T06:01:07+00:00"
1062 | },
1063 | {
1064 | "name": "sebastian/environment",
1065 | "version": "4.2.2",
1066 | "source": {
1067 | "type": "git",
1068 | "url": "https://github.com/sebastianbergmann/environment.git",
1069 | "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
1070 | },
1071 | "dist": {
1072 | "type": "zip",
1073 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
1074 | "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
1075 | "shasum": ""
1076 | },
1077 | "require": {
1078 | "php": "^7.1"
1079 | },
1080 | "require-dev": {
1081 | "phpunit/phpunit": "^7.5"
1082 | },
1083 | "suggest": {
1084 | "ext-posix": "*"
1085 | },
1086 | "type": "library",
1087 | "extra": {
1088 | "branch-alias": {
1089 | "dev-master": "4.2-dev"
1090 | }
1091 | },
1092 | "autoload": {
1093 | "classmap": [
1094 | "src/"
1095 | ]
1096 | },
1097 | "notification-url": "https://packagist.org/downloads/",
1098 | "license": [
1099 | "BSD-3-Clause"
1100 | ],
1101 | "authors": [
1102 | {
1103 | "name": "Sebastian Bergmann",
1104 | "email": "sebastian@phpunit.de"
1105 | }
1106 | ],
1107 | "description": "Provides functionality to handle HHVM/PHP environments",
1108 | "homepage": "http://www.github.com/sebastianbergmann/environment",
1109 | "keywords": [
1110 | "Xdebug",
1111 | "environment",
1112 | "hhvm"
1113 | ],
1114 | "time": "2019-05-05T09:05:15+00:00"
1115 | },
1116 | {
1117 | "name": "sebastian/exporter",
1118 | "version": "3.1.0",
1119 | "source": {
1120 | "type": "git",
1121 | "url": "https://github.com/sebastianbergmann/exporter.git",
1122 | "reference": "234199f4528de6d12aaa58b612e98f7d36adb937"
1123 | },
1124 | "dist": {
1125 | "type": "zip",
1126 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937",
1127 | "reference": "234199f4528de6d12aaa58b612e98f7d36adb937",
1128 | "shasum": ""
1129 | },
1130 | "require": {
1131 | "php": "^7.0",
1132 | "sebastian/recursion-context": "^3.0"
1133 | },
1134 | "require-dev": {
1135 | "ext-mbstring": "*",
1136 | "phpunit/phpunit": "^6.0"
1137 | },
1138 | "type": "library",
1139 | "extra": {
1140 | "branch-alias": {
1141 | "dev-master": "3.1.x-dev"
1142 | }
1143 | },
1144 | "autoload": {
1145 | "classmap": [
1146 | "src/"
1147 | ]
1148 | },
1149 | "notification-url": "https://packagist.org/downloads/",
1150 | "license": [
1151 | "BSD-3-Clause"
1152 | ],
1153 | "authors": [
1154 | {
1155 | "name": "Jeff Welch",
1156 | "email": "whatthejeff@gmail.com"
1157 | },
1158 | {
1159 | "name": "Volker Dusch",
1160 | "email": "github@wallbash.com"
1161 | },
1162 | {
1163 | "name": "Bernhard Schussek",
1164 | "email": "bschussek@2bepublished.at"
1165 | },
1166 | {
1167 | "name": "Sebastian Bergmann",
1168 | "email": "sebastian@phpunit.de"
1169 | },
1170 | {
1171 | "name": "Adam Harvey",
1172 | "email": "aharvey@php.net"
1173 | }
1174 | ],
1175 | "description": "Provides the functionality to export PHP variables for visualization",
1176 | "homepage": "http://www.github.com/sebastianbergmann/exporter",
1177 | "keywords": [
1178 | "export",
1179 | "exporter"
1180 | ],
1181 | "time": "2017-04-03T13:19:02+00:00"
1182 | },
1183 | {
1184 | "name": "sebastian/global-state",
1185 | "version": "3.0.0",
1186 | "source": {
1187 | "type": "git",
1188 | "url": "https://github.com/sebastianbergmann/global-state.git",
1189 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4"
1190 | },
1191 | "dist": {
1192 | "type": "zip",
1193 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
1194 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
1195 | "shasum": ""
1196 | },
1197 | "require": {
1198 | "php": "^7.2",
1199 | "sebastian/object-reflector": "^1.1.1",
1200 | "sebastian/recursion-context": "^3.0"
1201 | },
1202 | "require-dev": {
1203 | "ext-dom": "*",
1204 | "phpunit/phpunit": "^8.0"
1205 | },
1206 | "suggest": {
1207 | "ext-uopz": "*"
1208 | },
1209 | "type": "library",
1210 | "extra": {
1211 | "branch-alias": {
1212 | "dev-master": "3.0-dev"
1213 | }
1214 | },
1215 | "autoload": {
1216 | "classmap": [
1217 | "src/"
1218 | ]
1219 | },
1220 | "notification-url": "https://packagist.org/downloads/",
1221 | "license": [
1222 | "BSD-3-Clause"
1223 | ],
1224 | "authors": [
1225 | {
1226 | "name": "Sebastian Bergmann",
1227 | "email": "sebastian@phpunit.de"
1228 | }
1229 | ],
1230 | "description": "Snapshotting of global state",
1231 | "homepage": "http://www.github.com/sebastianbergmann/global-state",
1232 | "keywords": [
1233 | "global state"
1234 | ],
1235 | "time": "2019-02-01T05:30:01+00:00"
1236 | },
1237 | {
1238 | "name": "sebastian/object-enumerator",
1239 | "version": "3.0.3",
1240 | "source": {
1241 | "type": "git",
1242 | "url": "https://github.com/sebastianbergmann/object-enumerator.git",
1243 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5"
1244 | },
1245 | "dist": {
1246 | "type": "zip",
1247 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5",
1248 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5",
1249 | "shasum": ""
1250 | },
1251 | "require": {
1252 | "php": "^7.0",
1253 | "sebastian/object-reflector": "^1.1.1",
1254 | "sebastian/recursion-context": "^3.0"
1255 | },
1256 | "require-dev": {
1257 | "phpunit/phpunit": "^6.0"
1258 | },
1259 | "type": "library",
1260 | "extra": {
1261 | "branch-alias": {
1262 | "dev-master": "3.0.x-dev"
1263 | }
1264 | },
1265 | "autoload": {
1266 | "classmap": [
1267 | "src/"
1268 | ]
1269 | },
1270 | "notification-url": "https://packagist.org/downloads/",
1271 | "license": [
1272 | "BSD-3-Clause"
1273 | ],
1274 | "authors": [
1275 | {
1276 | "name": "Sebastian Bergmann",
1277 | "email": "sebastian@phpunit.de"
1278 | }
1279 | ],
1280 | "description": "Traverses array structures and object graphs to enumerate all referenced objects",
1281 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
1282 | "time": "2017-08-03T12:35:26+00:00"
1283 | },
1284 | {
1285 | "name": "sebastian/object-reflector",
1286 | "version": "1.1.1",
1287 | "source": {
1288 | "type": "git",
1289 | "url": "https://github.com/sebastianbergmann/object-reflector.git",
1290 | "reference": "773f97c67f28de00d397be301821b06708fca0be"
1291 | },
1292 | "dist": {
1293 | "type": "zip",
1294 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be",
1295 | "reference": "773f97c67f28de00d397be301821b06708fca0be",
1296 | "shasum": ""
1297 | },
1298 | "require": {
1299 | "php": "^7.0"
1300 | },
1301 | "require-dev": {
1302 | "phpunit/phpunit": "^6.0"
1303 | },
1304 | "type": "library",
1305 | "extra": {
1306 | "branch-alias": {
1307 | "dev-master": "1.1-dev"
1308 | }
1309 | },
1310 | "autoload": {
1311 | "classmap": [
1312 | "src/"
1313 | ]
1314 | },
1315 | "notification-url": "https://packagist.org/downloads/",
1316 | "license": [
1317 | "BSD-3-Clause"
1318 | ],
1319 | "authors": [
1320 | {
1321 | "name": "Sebastian Bergmann",
1322 | "email": "sebastian@phpunit.de"
1323 | }
1324 | ],
1325 | "description": "Allows reflection of object attributes, including inherited and non-public ones",
1326 | "homepage": "https://github.com/sebastianbergmann/object-reflector/",
1327 | "time": "2017-03-29T09:07:27+00:00"
1328 | },
1329 | {
1330 | "name": "sebastian/recursion-context",
1331 | "version": "3.0.0",
1332 | "source": {
1333 | "type": "git",
1334 | "url": "https://github.com/sebastianbergmann/recursion-context.git",
1335 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8"
1336 | },
1337 | "dist": {
1338 | "type": "zip",
1339 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
1340 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
1341 | "shasum": ""
1342 | },
1343 | "require": {
1344 | "php": "^7.0"
1345 | },
1346 | "require-dev": {
1347 | "phpunit/phpunit": "^6.0"
1348 | },
1349 | "type": "library",
1350 | "extra": {
1351 | "branch-alias": {
1352 | "dev-master": "3.0.x-dev"
1353 | }
1354 | },
1355 | "autoload": {
1356 | "classmap": [
1357 | "src/"
1358 | ]
1359 | },
1360 | "notification-url": "https://packagist.org/downloads/",
1361 | "license": [
1362 | "BSD-3-Clause"
1363 | ],
1364 | "authors": [
1365 | {
1366 | "name": "Jeff Welch",
1367 | "email": "whatthejeff@gmail.com"
1368 | },
1369 | {
1370 | "name": "Sebastian Bergmann",
1371 | "email": "sebastian@phpunit.de"
1372 | },
1373 | {
1374 | "name": "Adam Harvey",
1375 | "email": "aharvey@php.net"
1376 | }
1377 | ],
1378 | "description": "Provides functionality to recursively process PHP variables",
1379 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
1380 | "time": "2017-03-03T06:23:57+00:00"
1381 | },
1382 | {
1383 | "name": "sebastian/resource-operations",
1384 | "version": "2.0.1",
1385 | "source": {
1386 | "type": "git",
1387 | "url": "https://github.com/sebastianbergmann/resource-operations.git",
1388 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
1389 | },
1390 | "dist": {
1391 | "type": "zip",
1392 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
1393 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
1394 | "shasum": ""
1395 | },
1396 | "require": {
1397 | "php": "^7.1"
1398 | },
1399 | "type": "library",
1400 | "extra": {
1401 | "branch-alias": {
1402 | "dev-master": "2.0-dev"
1403 | }
1404 | },
1405 | "autoload": {
1406 | "classmap": [
1407 | "src/"
1408 | ]
1409 | },
1410 | "notification-url": "https://packagist.org/downloads/",
1411 | "license": [
1412 | "BSD-3-Clause"
1413 | ],
1414 | "authors": [
1415 | {
1416 | "name": "Sebastian Bergmann",
1417 | "email": "sebastian@phpunit.de"
1418 | }
1419 | ],
1420 | "description": "Provides a list of PHP built-in functions that operate on resources",
1421 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
1422 | "time": "2018-10-04T04:07:39+00:00"
1423 | },
1424 | {
1425 | "name": "sebastian/version",
1426 | "version": "2.0.1",
1427 | "source": {
1428 | "type": "git",
1429 | "url": "https://github.com/sebastianbergmann/version.git",
1430 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
1431 | },
1432 | "dist": {
1433 | "type": "zip",
1434 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019",
1435 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
1436 | "shasum": ""
1437 | },
1438 | "require": {
1439 | "php": ">=5.6"
1440 | },
1441 | "type": "library",
1442 | "extra": {
1443 | "branch-alias": {
1444 | "dev-master": "2.0.x-dev"
1445 | }
1446 | },
1447 | "autoload": {
1448 | "classmap": [
1449 | "src/"
1450 | ]
1451 | },
1452 | "notification-url": "https://packagist.org/downloads/",
1453 | "license": [
1454 | "BSD-3-Clause"
1455 | ],
1456 | "authors": [
1457 | {
1458 | "name": "Sebastian Bergmann",
1459 | "email": "sebastian@phpunit.de",
1460 | "role": "lead"
1461 | }
1462 | ],
1463 | "description": "Library that helps with managing the version number of Git-hosted PHP projects",
1464 | "homepage": "https://github.com/sebastianbergmann/version",
1465 | "time": "2016-10-03T07:35:21+00:00"
1466 | },
1467 | {
1468 | "name": "symfony/polyfill-ctype",
1469 | "version": "v1.11.0",
1470 | "source": {
1471 | "type": "git",
1472 | "url": "https://github.com/symfony/polyfill-ctype.git",
1473 | "reference": "82ebae02209c21113908c229e9883c419720738a"
1474 | },
1475 | "dist": {
1476 | "type": "zip",
1477 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a",
1478 | "reference": "82ebae02209c21113908c229e9883c419720738a",
1479 | "shasum": ""
1480 | },
1481 | "require": {
1482 | "php": ">=5.3.3"
1483 | },
1484 | "suggest": {
1485 | "ext-ctype": "For best performance"
1486 | },
1487 | "type": "library",
1488 | "extra": {
1489 | "branch-alias": {
1490 | "dev-master": "1.11-dev"
1491 | }
1492 | },
1493 | "autoload": {
1494 | "psr-4": {
1495 | "Symfony\\Polyfill\\Ctype\\": ""
1496 | },
1497 | "files": [
1498 | "bootstrap.php"
1499 | ]
1500 | },
1501 | "notification-url": "https://packagist.org/downloads/",
1502 | "license": [
1503 | "MIT"
1504 | ],
1505 | "authors": [
1506 | {
1507 | "name": "Symfony Community",
1508 | "homepage": "https://symfony.com/contributors"
1509 | },
1510 | {
1511 | "name": "Gert de Pagter",
1512 | "email": "BackEndTea@gmail.com"
1513 | }
1514 | ],
1515 | "description": "Symfony polyfill for ctype functions",
1516 | "homepage": "https://symfony.com",
1517 | "keywords": [
1518 | "compatibility",
1519 | "ctype",
1520 | "polyfill",
1521 | "portable"
1522 | ],
1523 | "time": "2019-02-06T07:57:58+00:00"
1524 | },
1525 | {
1526 | "name": "theseer/tokenizer",
1527 | "version": "1.1.2",
1528 | "source": {
1529 | "type": "git",
1530 | "url": "https://github.com/theseer/tokenizer.git",
1531 | "reference": "1c42705be2b6c1de5904f8afacef5895cab44bf8"
1532 | },
1533 | "dist": {
1534 | "type": "zip",
1535 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/1c42705be2b6c1de5904f8afacef5895cab44bf8",
1536 | "reference": "1c42705be2b6c1de5904f8afacef5895cab44bf8",
1537 | "shasum": ""
1538 | },
1539 | "require": {
1540 | "ext-dom": "*",
1541 | "ext-tokenizer": "*",
1542 | "ext-xmlwriter": "*",
1543 | "php": "^7.0"
1544 | },
1545 | "type": "library",
1546 | "autoload": {
1547 | "classmap": [
1548 | "src/"
1549 | ]
1550 | },
1551 | "notification-url": "https://packagist.org/downloads/",
1552 | "license": [
1553 | "BSD-3-Clause"
1554 | ],
1555 | "authors": [
1556 | {
1557 | "name": "Arne Blankerts",
1558 | "email": "arne@blankerts.de",
1559 | "role": "Developer"
1560 | }
1561 | ],
1562 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
1563 | "time": "2019-04-04T09:56:43+00:00"
1564 | },
1565 | {
1566 | "name": "webmozart/assert",
1567 | "version": "1.4.0",
1568 | "source": {
1569 | "type": "git",
1570 | "url": "https://github.com/webmozart/assert.git",
1571 | "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
1572 | },
1573 | "dist": {
1574 | "type": "zip",
1575 | "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
1576 | "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
1577 | "shasum": ""
1578 | },
1579 | "require": {
1580 | "php": "^5.3.3 || ^7.0",
1581 | "symfony/polyfill-ctype": "^1.8"
1582 | },
1583 | "require-dev": {
1584 | "phpunit/phpunit": "^4.6",
1585 | "sebastian/version": "^1.0.1"
1586 | },
1587 | "type": "library",
1588 | "extra": {
1589 | "branch-alias": {
1590 | "dev-master": "1.3-dev"
1591 | }
1592 | },
1593 | "autoload": {
1594 | "psr-4": {
1595 | "Webmozart\\Assert\\": "src/"
1596 | }
1597 | },
1598 | "notification-url": "https://packagist.org/downloads/",
1599 | "license": [
1600 | "MIT"
1601 | ],
1602 | "authors": [
1603 | {
1604 | "name": "Bernhard Schussek",
1605 | "email": "bschussek@gmail.com"
1606 | }
1607 | ],
1608 | "description": "Assertions to validate method input/output with nice error messages.",
1609 | "keywords": [
1610 | "assert",
1611 | "check",
1612 | "validate"
1613 | ],
1614 | "time": "2018-12-25T11:19:39+00:00"
1615 | }
1616 | ],
1617 | "aliases": [],
1618 | "minimum-stability": "stable",
1619 | "stability-flags": [],
1620 | "prefer-stable": false,
1621 | "prefer-lowest": false,
1622 | "platform": [],
1623 | "platform-dev": []
1624 | }
1625 |
--------------------------------------------------------------------------------
/tests/Parser/Visitor/DocBlockTypeAnnotationsTest.php:
--------------------------------------------------------------------------------
1 | parseAndTraverseFileContents(__DIR__ . '/Example/TestClass.php', $visitor);
41 | $this->extracted = $visitor->getNamespaces();
42 | }
43 |
44 |
45 | public function testExtractsArgumentAnnotations(): void
46 | {
47 | $this->assertContains(DocBlockArgument::class, $this->extracted);
48 | }
49 |
50 | public function testExtractsImportedArgumentAnnotations(): void
51 | {
52 | $this->assertContains(DocBlockReturn::class, $this->extracted);
53 | }
54 |
55 | public function testExtractsReturnTypeAnnotations(): void
56 | {
57 | $this->assertContains(ImportedDocBlockArgument::class, $this->extracted);
58 | }
59 |
60 | public function testExtractsImportedReturnTypeAnnotations(): void
61 | {
62 | $this->assertContains(ImportedDocBlockReturn::class, $this->extracted);
63 | }
64 |
65 | public function testExtractsArgumentAnnotationsFromAnonymousClass()
66 | {
67 | $this->assertContains(AnonymousClassDocBlockArgument::class, $this->extracted);
68 | }
69 |
70 | public function testExtractsImportedArgumentAnnotationFromAnonymousClass()
71 | {
72 | $this->assertContains(ImportedAnonymousClassDocBlockArgument::class, $this->extracted);
73 | }
74 |
75 | public function testIgnoresScalarTypeHints()
76 | {
77 | $this->assertNotContains('string', $this->extracted);
78 | $this->assertNotContains('int', $this->extracted);
79 | }
80 |
81 | public function testIncludesNonExistingClasses()
82 | {
83 | $this->assertContains('Non\\ExistingClass', $this->extracted);
84 | $this->assertContains('Another\\Non\\ExistingClass', $this->extracted);
85 | }
86 |
87 | public function testParsesReferencesToItselfCorrectly()
88 | {
89 | $this->assertContains(TestClass::class, $this->extracted);
90 | }
91 |
92 | public function testIncludesGenericClasses()
93 | {
94 | $this->assertContains(ImportedGenericClassArgument::class, $this->extracted);
95 | $this->assertContains(ImportedGenericArgument::class, $this->extracted);
96 | }
97 |
98 | public function testIncludesUnionTypesFromDocBlocks(): void
99 | {
100 | $this->assertContains(DocBlockUnionTypeA::class, $this->extracted);
101 | $this->assertContains(DocBlockUnionTypeB::class, $this->extracted);
102 | }
103 |
104 | public function testResolvesUnionsWhenCombinedWithGenerics(): void
105 | {
106 | $this->assertContains(DocBlockUnionGenericWrapperA::class, $this->extracted);
107 | $this->assertContains(DocBlockUnionGenericWrapperB::class, $this->extracted);
108 | $this->assertContains(DocBlockUnionGenericChildA::class, $this->extracted);
109 | $this->assertContains(DocBlockUnionGenericChildB::class, $this->extracted);
110 | $this->assertContains(DocBlockUnionGenericChildC::class, $this->extracted);
111 | $this->assertContains(DocBlockUnionGenericChildD::class, $this->extracted);
112 | }
113 |
114 | public function testResolvesArrayTypes(): void
115 | {
116 | $this->assertContains(DocBlockArrayItem::class, $this->extracted);
117 | }
118 |
119 | public function testIgnoresUntypedTemplatesInDocBlocks(): void
120 | {
121 | $this->assertNotContains('UntypedTemplateInDocblock', $this->extracted);
122 | }
123 |
124 | public function testResolvesTypedTemplateTagsToBaseType(): void
125 | {
126 | $this->assertNotContains('TypedTemplateInDocblock', $this->extracted);
127 | $this->assertContains(DocBlockTypedTemplate::class, $this->extracted);
128 | }
129 |
130 | public function testResolvesShortStyleGenericShortArraySyntax(): void
131 | {
132 | $this->assertContains(DocBlockGenericTypeArrayShort::class, $this->extracted);
133 | }
134 |
135 | public function testResolvesShortStyleGenericLongArraySyntax(): void
136 | {
137 | $this->assertContains(DocBlockGenericTypeArrayLong::class, $this->extracted);
138 | }
139 |
140 | public function testResolvesGenericPseudoTypes(): void
141 | {
142 | $this->assertContains(GenericPseudoType::class, $this->extracted);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/tests/Parser/Visitor/Example/DocBlock/AnonymousClassDocBlockArgument.php:
--------------------------------------------------------------------------------
1 | $argument
142 | * @return ImportedGenericClassArgument
143 | */
144 | public function genericsInDocBlock($argument)
145 | {
146 |
147 | }
148 |
149 | /**
150 | * @return DocBlockUnionTypeA|DocBlockUnionTypeB
151 | */
152 | public function unionTypeInDocBlock()
153 | {
154 |
155 | }
156 |
157 | /**
158 | * @return DocBlockUnionGenericWrapperA|DocBlockUnionGenericWrapperB
159 | */
160 | public function unionAndGenericInDocBlock()
161 | {
162 |
163 | }
164 |
165 | /**
166 | * @return DocBlockArrayItem[]
167 | */
168 | public function docBlockArray()
169 | {
170 |
171 | }
172 | /**
173 | * @return array
174 | */
175 | public function docBlockArrayGenericStyleShort()
176 | {
177 |
178 | }
179 |
180 | /**
181 | * @return array
182 | */
183 | public function docBlockArraydocBlockArrayGenericStyleLong()
184 | {
185 |
186 | }
187 |
188 | /**
189 | * @return list
190 | */
191 | public function docBlockGenericPseudoType()
192 | {
193 |
194 | }
195 |
196 | /**
197 | * @template UntypedTemplateInDocblock
198 | * @return UntypedTemplateInDocblock
199 | */
200 | public function docBlockUntypedTemplate()
201 | {
202 |
203 | }
204 |
205 | /**
206 | * @template TypedTemplateInDocblock of DocBlockTypedTemplate
207 | * @return TypedTemplateInDocblock
208 | */
209 | public function docBlockTypedTemplate()
210 | {
211 |
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/tests/Parser/Visitor/Example/Traits/ImporetdTrait.php:
--------------------------------------------------------------------------------
1 | parseAndTraverseFileContents(__DIR__ . '/Example/TestClass.php', $visitor);
16 | $this->assertEquals(TestClass::class, $visitor->getDeclared());
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/tests/Parser/Visitor/FullyQualifiedReferencedTest.php:
--------------------------------------------------------------------------------
1 | parseAndTraverseFileContents(__DIR__ . '/Example/TestClass.php', $visitor);
25 | $this->extracted = $visitor->getNamespaces();
26 | }
27 |
28 |
29 | public function testShouldCollectClassExtensions(): void
30 | {
31 | $this->assertContains(ParentClass::class, $this->extracted);
32 | }
33 |
34 | public function testShouldCollectInterfaceImplementations(): void
35 | {
36 | $this->assertContains(SomeInterface::class, $this->extracted);
37 | }
38 |
39 |
40 | public function testShouldCollectInstanceCreation(): void
41 | {
42 | $this->assertContains(Example\InstanceCreation\InstanceCreation::class, $this->extracted);
43 | }
44 |
45 | public function testShouldCollectInstanceCreationOfImportedClasses(): void
46 | {
47 | $this->assertContains(ImportedInstanceCreation::class, $this->extracted);
48 | }
49 |
50 |
51 | public function testShouldCollectStaticMethodCalls(): void
52 | {
53 | $this->assertContains(Example\StaticMethodCall\StaticMethodCall::class, $this->extracted);
54 | }
55 |
56 |
57 | public function testShouldCollectStaticMethodCallsFromImportedClasses(): void
58 | {
59 | $this->assertContains(ImportedStaticMethodCall::class, $this->extracted);
60 | }
61 |
62 | public function testCollectsUsedTraits(): void
63 | {
64 | $this->assertContains(UsedTrait::class, $this->extracted);
65 | }
66 |
67 | public function testCollectsUsedImportedTraits(): void
68 | {
69 | $this->assertContains(ImporetdTrait::class, $this->extracted);
70 | }
71 |
72 |
73 | public function testExtractsArgumentAnnotations(): void
74 | {
75 | $this->assertContains(Example\TypeAnnotation\ArgumentAnnotation::class, $this->extracted);
76 | }
77 |
78 | public function testExtractsImportedArgumentAnnotations(): void
79 | {
80 | $this->assertContains(Example\TypeAnnotation\ImportedArgumentAnnotation::class, $this->extracted);
81 | }
82 |
83 | public function testExtractsReturnTypeAnnotations(): void
84 | {
85 | $this->assertContains(Example\TypeAnnotation\ReturnTypeAnnotation::class, $this->extracted);
86 | }
87 |
88 | public function testExtractsImportedReturnTypeAnnotations(): void
89 | {
90 | $this->assertContains(Example\TypeAnnotation\ImportedReturnTypeAnnotation::class, $this->extracted);
91 | }
92 |
93 | public function testIgnoresScalarTypeHints()
94 | {
95 | $this->assertNotContains('string', $this->extracted);
96 | $this->assertNotContains('int', $this->extracted);
97 | }
98 |
99 | public function testIgnoresSimpleFunctionCalls()
100 | {
101 | $this->assertNotContains('count', $this->extracted);
102 | }
103 |
104 | public function testIncludesNonExistingClasses()
105 | {
106 | $this->assertContains('Foo\Bar\This\Does\Not\Exist', $this->extracted);
107 | }
108 |
109 | public function testParsesReferencesToItselfCorrectly()
110 | {
111 | $this->assertContains(TestClass::class, $this->extracted);
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/tests/Parser/Visitor/UseStatementTest.php:
--------------------------------------------------------------------------------
1 | parseAndTraverseFileContents(__DIR__ . '/Example/TestClass.php', $visitor);
15 | $this->assertContains(
16 | 'Foo\\Bar\\Baz',
17 | $visitor->getNamespaces()
18 | );
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | create(ParserFactory::PREFER_PHP7)
21 | ->parse(file_get_contents($fileName));
22 | }
23 |
24 | protected function parseAndTraverseFileContents(string $fileName, NodeVisitor $visitor): void
25 | {
26 | $ast = $this->parsePhpFileContents($fileName);
27 | $traverser = new NodeTraverser();
28 | $traverser->addVisitor(new NameResolver());
29 | $traverser->addVisitor($visitor);
30 | $traverser->traverse($ast);
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/tests/Utility/NamespaceComperatorCollectionTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(
20 | $matches,
21 | (new NamespaceComperatorCollection($base))->containsAny($compared)
22 | );
23 | }
24 |
25 | public function getNamespaces(): array
26 | {
27 | return [
28 | // Collection with 1 comperator
29 | [ true, [ 'App\\' ], 'App\\Utility\\Test' ],
30 | [ false, [ 'App\\' ], 'Lib\\Utility\\Test' ],
31 | // Collections with multiple comperators
32 | [ true, [ 'App\\Ns1', 'App\\Ns2' ], 'App\\Ns1\\Test' ],
33 | [ true, [ 'App\\Ns1', 'App\\Ns2' ], 'App\\Ns2\\Test' ],
34 | [ false, [ 'App\\Ns1', 'App\\Ns2' ], 'App\\Ns3\\Test' ],
35 | ];
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/tests/Utility/NamespaceComperatorTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'Test\\Test',
14 | (new NamespaceComperator('\\Test\\Test\\'))->__toString()
15 | );
16 | }
17 |
18 | /**
19 | * @param bool $matches
20 | * @param string $base
21 | * @param string $compared
22 | * @dataProvider getNamespaces
23 | */
24 | public function testContainsNamespace(bool $matches, string $base, string $compared): void
25 | {
26 | $this->assertEquals(
27 | $matches,
28 | (new NamespaceComperator($base))->contains($compared)
29 | );
30 | }
31 |
32 | public function getNamespaces(): array
33 | {
34 | return [
35 | // direct match
36 | [ true, 'MyVendor\\MyPackage\\MyComponent', 'MyVendor\\MyPackage\\MyComponent' ],
37 | // Subclass / namespace
38 | [ true, 'MyVendor\\MyPackage', 'MyVendor\\MyPackage\\MyComponent' ],
39 | // Leading & trailing backslashes
40 | [ true, 'MyVendor\\MyPackage', '\\MyVendor\\MyPackage\\MyComponent' ],
41 | [ true, 'MyVendor\\MyPackage', 'MyVendor\\MyPackage\\MyComponent\\' ],
42 | [ true, 'MyVendor\\MyPackage', '\\MyVendor\\MyPackage\\MyComponent\\' ],
43 | [ true, '\\MyVendor\\MyPackage', '\\MyVendor\\MyPackage\\MyComponent\\' ],
44 | [ true, 'MyVendor\\MyPackage\\', '\\MyVendor\\MyPackage\\MyComponent\\' ],
45 | [ true, '\\MyVendor\\MyPackage\\', '\\MyVendor\\MyPackage\\MyComponent\\' ],
46 | // Whole different namespace altogether
47 | [ false, 'MyVendor\\MyPackage\\MyComponent', 'MyVendor\\MyPackage\\MyOtherComponent' ],
48 | // Search is more specific than compared
49 | [ false, 'MyVendor\\MyPackage\\MyComponent', 'MyVendor\\MyPackage' ]
50 | ];
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Validation/ForbiddenDependencyTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(
27 | $isValid,
28 | (new ForbiddenDependency($definitionFrom, $definitionTo))->isValidBetween($testFrom, $testTo)
29 | );
30 | }
31 |
32 | public function getDependencyDefinitions(): array
33 | {
34 | return [
35 | [ 'Foo\\Bar', 'Foo\\Baz', 'Foo\\Bar\\Component', 'Foo\\Baz\\Component', false ],
36 | [ 'Foo\\Bar', 'Foo\\Baz', 'Foo\\Bar\\Component', 'Foo\\Bar\\Another', true ],
37 | [ 'Foo\\Bar', 'Foo\\Baz', 'Foo\\Bar\\Component', 'Completely\\Outside', true ],
38 | [ 'Foo\\Bar', 'Foo\\Baz', 'Foo\\Bar\\Component', 'Foo\\Baz', false ],
39 | [ 'Foo\\Bar', 'Foo\\Baz', 'Completely\\Unrelated', 'Foo\\Baz', true ],
40 | ];
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/tests/Validation/Mock/ExistingClass.php:
--------------------------------------------------------------------------------
1 | result = $result;
16 | $this->message = $message;
17 | }
18 |
19 | public function isValidBetween(string $from, string $to): bool
20 | {
21 | return $this->result;
22 | }
23 |
24 | public function getErrorMessage(string $from, string $to): array
25 | {
26 | return [ $this->message ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/Validation/MustBeSelfContainedTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(
25 | $isValid,
26 | (new MustBeSelfContained($definition))->isValidBetween($testFrom, $testTo)
27 | );
28 | }
29 |
30 | public function getDependencyDefinitions(): array
31 | {
32 | return [
33 | [ 'Foo\\Bar', 'Foo\\Bar\\Component', 'Foo\\Baz\\Component', false ],
34 | [ 'Foo\\Bar', 'Foo\\Bar\\Component', 'Foo\\Bar\\Another', true ],
35 | [ 'Foo\\Bar', 'Foo\\Bar\\Component', 'Completely\\Outside', false ],
36 | [ 'Foo\\Bar', 'Foo\\Bar\\Component', 'Foo\\Baz', false ],
37 | [ 'Foo\\Bar', 'Completely\\Outside', 'Not\\Related', true ]
38 | ];
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/tests/Validation/MustOnlyDependOnTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(
27 | $isValid,
28 | (new MustOnlyDependOn($definitionFrom, $definitionTo))->isValidBetween($testFrom, $testTo)
29 | );
30 | }
31 |
32 | public function getDependencyData(): array
33 | {
34 | return [
35 | [ 'Foo\\Bar', 'Bar\\Baz', 'Foo\\Bar\\Component', 'Foo\\Bar\\InSameNamespace', true ],
36 | [ 'Foo\\Bar', 'Bar\\Baz', 'Foo\\Bar\\Component', 'Bar\\Baz\\InOtherNamespace', true ],
37 | [ 'Foo\\Bar', 'Bar\\Baz', 'Foo\\Bar\\Component', 'Completely\\Outside', false ],
38 | [ 'Foo\\Bar', 'Bar\\Baz', 'Completely\\Outside', 'Foo\\Bar\\InSameNamespace', true ],
39 | [ 'Foo\\Bar', 'Bar\\Baz', 'Completely\\Outside', 'Bar\\Baz\\InOtherNamespace', true ],
40 | [ 'Foo\\Bar', 'Bar\\Baz', 'Completely\\Outside', 'Also\\Outside', true ],
41 | [ 'Foo\\Bar', [ 'Bar', 'Baz' ], 'Foo\\Bar\\Component', 'Bar\\InFirstAllowed', true ],
42 | [ 'Foo\\Bar', [ 'Bar', 'Baz' ], 'Foo\\Bar\\Component', 'Baz\\InSecondAllowed', true ],
43 | [ 'Foo\\Bar', [ 'Bar', 'Baz' ], 'Foo\\Bar\\Component', 'Completely\\Outside', false ],
44 | ];
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Validation/MustOnlyHaveAutoloadableDependenciesTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($isValid, (new MustOnlyHaveAutoloadableDependencies())->isValidBetween($from, $to));
23 | }
24 |
25 | public function getDependencyData(): array
26 | {
27 | return [
28 | [ static::class, ExistingClass::class, true ],
29 | [ static::class, ExistingInterface::class, true ],
30 | [ static::class, ExistingTrait::class, true ],
31 | [ static::class, 'Non\\ExistingClass', false ],
32 | ];
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/tests/Validation/ValidationCollectionTest.php:
--------------------------------------------------------------------------------
1 | assertTrue((new ValidationCollection())->isValidBetween('Foo', 'Bar'));
16 | }
17 |
18 | public function testIsValidIfAllValidatorsAreValid(): void
19 | {
20 | $validator = new ValidationCollection();
21 | $validator->addValidator(new TestValidator(true));
22 | $validator->addValidator(new TestValidator(true));
23 | $this->assertTrue($validator->isValidBetween('Foo', 'Bar'));
24 | }
25 |
26 | public function testIsValidIfAtLeastOneValidatorIsInvalid(): void
27 | {
28 | $validator = new ValidationCollection();
29 | $validator->addValidator(new TestValidator(false));
30 | $validator->addValidator(new TestValidator(true));
31 | $this->assertFalse($validator->isValidBetween('Foo', 'Bar'));
32 | }
33 |
34 | public function testReturnsErrorsOfAllValidators()
35 | {
36 | $validator = new ValidationCollection();
37 | $validator->addValidator(new TestValidator(false, 'Foo'));
38 | $validator->addValidator(new TestValidator(true, 'Bar'));
39 | $validator->addValidator(new TestValidator(false, 'Baz'));
40 | $this->assertFalse($validator->isValidBetween('Test', 'PhpArch'));
41 | $this->assertContains('Foo', $validator->getErrorMessage('Test', 'PhpArch'));
42 | $this->assertNotContains('Bar', $validator->getErrorMessage('Test', 'PhpArch'));
43 | $this->assertContains('Baz', $validator->getErrorMessage('Test', 'PhpArch'));
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------