├── .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 [![Build Status](https://travis-ci.org/j6s/phparch.svg?branch=development)](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 | --------------------------------------------------------------------------------