├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CNAME ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── _config.yml ├── bin ├── check-code-cov ├── dephpend ├── pdepend-xml-filter ├── php-trace ├── prepare-tag └── qa ├── box.json.dist ├── composer.json ├── config ├── pre-commit └── pre-push ├── doc ├── README.md ├── Scope.md ├── dephpend-timeline.gif └── logo.svg ├── phpunit.xml.dist ├── src ├── Analyser │ ├── DefaultParser.php │ ├── DependencyInspectionVisitor.php │ ├── Metrics.php │ ├── Parser.php │ ├── StaticAnalyser.php │ └── XDebugFunctionTraceAnalyser.php ├── Cli │ ├── Application.php │ ├── BaseCommand.php │ ├── Dispatcher.php │ ├── DotCommand.php │ ├── DsmCommand.php │ ├── ErrorOutput.php │ ├── MetricsCommand.php │ ├── TestFeaturesCommand.php │ ├── TextCommand.php │ └── UmlCommand.php ├── Dependencies │ ├── AbstractClazz.php │ ├── Clazz.php │ ├── ClazzLike.php │ ├── Dependency.php │ ├── DependencyFactory.php │ ├── DependencyFilter.php │ ├── DependencyMap.php │ ├── DependencySet.php │ ├── Interfaze.php │ ├── Namespaze.php │ ├── NullDependency.php │ └── Trait_.php ├── Exceptions │ ├── DotNotInstalledException.php │ ├── FileDoesNotExistException.php │ ├── FileIsNotReadableException.php │ ├── ParserException.php │ └── PlantUmlNotInstalledException.php ├── Formatters │ ├── DependencyStructureMatrixBuilder.php │ ├── DependencyStructureMatrixHtmlFormatter.php │ ├── DotFormatter.php │ ├── Formatter.php │ └── PlantUmlFormatter.php ├── OS │ ├── DotWrapper.php │ ├── PhpFile.php │ ├── PhpFileFinder.php │ ├── PhpFileSet.php │ ├── PlantUmlWrapper.php │ └── ShellWrapper.php └── Util │ ├── AbstractCollection.php │ ├── AbstractMap.php │ ├── Collection.php │ ├── DependencyContainer.php │ ├── Functional.php │ └── Util.php └── tests ├── feature ├── DsmTest.php ├── HelpTest.php ├── ListTest.php ├── MetricsTest.php ├── TextTest.php ├── UmlTest.php └── constants.php ├── samples ├── CallToStaticMethodFeature.php ├── CreatingObjectsFeature.php ├── CreatingObjectsFromStringsFeature.php ├── ExtendingOtherClassesFeature.php ├── ImplementingInterfacesFeature.php ├── KnownVariablePassedIntoMethodWithoutTypeHintsFeature.php ├── MethodArgumentsAndReturnValueFromDocFeature.php ├── ParamReturnThrowsInDocCommentFeature.php ├── Php7ReturnValueDeclarationFeature.php ├── ReturnValueOfKnownMethodFeature.php ├── TypeHintsInMethodArgumentsFeature.php └── UsingTraitsFeature.php └── unit ├── Analyser ├── DefaultParserTest.php ├── DependencyInspectionVisitorTest.php ├── MetricsTest.php ├── StaticAnalyserTest.php └── XDebugFunctionTraceAnalyserTest.php ├── Cli ├── ApplicationTest.php ├── DispatcherTest.php ├── DotCommandTest.php ├── DsmCommandTest.php ├── ErrorOutputTest.php ├── MetricsCommandTest.php ├── TextCommandTest.php └── UmlCommandTest.php ├── Dependencies ├── ClazzTest.php ├── DependencyFactoryTest.php ├── DependencyFilterTest.php ├── DependencyMapTest.php ├── DependencySetTest.php ├── NamespazeTest.php └── NullDependencyTest.php ├── DependencyHelper.php ├── DependencyHelperTest.php ├── Formatters ├── DependencyStructureMatrixBuilderTest.php ├── DependencyStructureMatrixHtmlFormatterTest.php ├── DotFormatterTest.php └── PlantUmlFormatterTest.php ├── OS ├── DotWrapperTest.php ├── PhpFileFinderTest.php ├── PhpFileSetTest.php ├── PhpFileTest.php ├── PlantUmlWrapperTest.php └── ShellWrapperTest.php └── Util ├── DependencyContainerTest.php ├── FunctionalTest.php └── UtilTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | # PHPStorm settings, etc. 2 | .idea 3 | 4 | # composer dependencies 5 | /vendor 6 | /samples 7 | 8 | *.cache 9 | 10 | # for now don't add any more .phar files 11 | # because they might bloat the repo 12 | *.phar 13 | 14 | # build artifacts 15 | /build 16 | 17 | # log files 18 | *.log 19 | 20 | composer.lock 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # TravisCI configuration for mihaeu/dephpend 2 | 3 | language: "php" 4 | os: 5 | - "linux" 6 | dist: "bionic" 7 | 8 | php: 9 | - "7.4" 10 | - "7.3" 11 | - "7.2" 12 | 13 | install: 14 | - "mkdir -p build/logs" 15 | - "composer validate --strict" 16 | - "composer install --prefer-dist" 17 | 18 | script: 19 | - "bin/check-code-cov" 20 | - "vendor/bin/phpunit -c phpunit.xml.dist --coverage-text --coverage-clover ./build/logs/clover.xml" 21 | 22 | after_script: 23 | - "wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.2/php-coveralls.phar" 24 | - "php php-coveralls.phar -v" 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.8.1] - 2021-05-02 10 | ### Fixed 11 | - Fixed `test-features` command 12 | 13 | ## [0.8.0] - 2021-05-02 14 | ### Added 15 | - PHP 8 support 16 | 17 | ## [0.7.0] - 2020-06-19 18 | ### Removed 19 | - Removed support for Symfony 2 and 3 20 | 21 | ## [0.6.3] - 2020-01-17 22 | ### Fixed 23 | - [Issue 58](https://github.com/mihaeu/dephpend/issues/58) 24 | 25 | ## [0.6.2] - 2019-10-20 26 | ### Fixed 27 | - Do not analyse method calls via dynamic property fetch [#50](https://github.com/mihaeu/dephpend/issues/56) 28 | 29 | ## [0.6.1] - 2019-07-14 30 | ### Added 31 | - Added filename to parser exception message 32 | 33 | ### Fixed 34 | - Fixed return type of edge case for abstractness metric 35 | - Fixed BC break in symfony/console 4.3 36 | 37 | ## [0.6.0] - 2019-04-09 38 | ### Added 39 | - support for Docker 40 | - Messages for missing PlantUML and Docker installation 41 | 42 | ### Changed 43 | - upgrade PHP language dependency to ^7.2 44 | - upgrade PHPUnit to ^8.0 45 | 46 | ## [0.5.1] - 2018-07-04 47 | ### Fixed 48 | - replaced Symfony Console hacks with injection through Event Dispatcher 49 | - PHP 7.2 support for projects which require PHP 7.2 and Symfony 4 components 50 | 51 | ## [0.5.1] - 2018-07-04 52 | ### Fixed 53 | - PHP 7.2 support for projects which require PHP 7.2 and Symfony 4 components 54 | 55 | ## [0.5.0] - 2018-03-23 56 | ### Added 57 | - PHP 7.2 support 58 | 59 | ### Fixed 60 | - better Windows support 61 | 62 | ## [0.4.0] - 2017-05-12 63 | ### Added 64 | - PHP 7.1 support 65 | - add detection of instanceof comparison 66 | - switch DSM column and row (like NDepend) 67 | 68 | ### Fixed 69 | - add support for inner classes 70 | 71 | ## [0.3.1] - 2016-11-16 72 | ### Fixed 73 | - default options like --help not working 74 | 75 | ## [0.3.0] - 2016-11-15 76 | ### Added 77 | - dot command for layered dependency graphs 78 | 79 | ### Changed 80 | - packages in UML diagrams will now be displayed hierarchical 81 | 82 | ### Fixed 83 | - removed empty dependencies after applying depth filter 84 | 85 | ## [0.2.0] - 2016-10-15 86 | ### Added 87 | - filter-from filter (filters only source dependencies, not the target) 88 | - exclude-regex (excludes any dependency pair which matches the regular expression) 89 | - dynamic analysis 90 | 91 | ### Changed 92 | - metrics are being displayed as a table 93 | 94 | ## [0.1.0] - 2016-10-01 95 | ### Added 96 | - first tagged release 97 | - uml, text, dsm and metrics command 98 | 99 | [Unreleased]: https://github.com/mihaeu/dephpend/compare/0.8.1...HEAD 100 | [0.8.0]: https://github.com/mihaeu/dephpend/compare/0.8.0...0.8.1 101 | [0.8.0]: https://github.com/mihaeu/dephpend/compare/0.7.0...0.8.0 102 | [0.7.0]: https://github.com/mihaeu/dephpend/compare/0.6.3...0.7.0 103 | [0.6.3]: https://github.com/mihaeu/dephpend/compare/0.6.2...0.6.3 104 | [0.6.2]: https://github.com/mihaeu/dephpend/compare/0.6.1...0.6.2 105 | [0.6.1]: https://github.com/mihaeu/dephpend/compare/0.6.0...0.6.1 106 | [0.6.0]: https://github.com/mihaeu/dephpend/compare/0.5.1...0.6.0 107 | [0.5.1]: https://github.com/mihaeu/dephpend/compare/0.5.0...0.5.1 108 | [0.5.0]: https://github.com/mihaeu/dephpend/compare/0.4.0...0.5.0 109 | [0.4.0]: https://github.com/mihaeu/dephpend/compare/0.3.2...0.4.0 110 | [0.3.2]: https://github.com/mihaeu/dephpend/compare/0.3.1...0.3.2 111 | [0.3.1]: https://github.com/mihaeu/dephpend/compare/0.3.0...0.3.1 112 | [0.3.0]: https://github.com/mihaeu/dephpend/compare/0.2.0...0.3.0 113 | [0.2.0]: https://github.com/mihaeu/dephpend/compare/0.1.0...0.2.0 114 | [0.1.0]: https://github.com/mihaeu/dephpend/compare/0549dbd...0.1.0 115 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | dephpend.com -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When submitting pull requests please follow the following guidelines: 4 | 5 | ## QA 6 | 7 | There are a few QA tests which every commit has to pass. Please install the following Git hooks: 8 | 9 | ```bash 10 | # from project root 11 | cp config/pre-commit .git/hooks/ 12 | cp config/pre-push .git/hooks/ 13 | ``` 14 | 15 | These hooks enforce coding style, code coverage, use of @covers annotation and checks for debug statements. On `pre-commit` only coding style is enforced, on `pre-push` everything has to pass. 16 | 17 | ## Git Commit Messages 18 | 19 | 1. Separate subject from body with a blank line 20 | 2. Limit the subject line to 50 characters 21 | 3. Capitalize the subject line 22 | 4. Do not end the subject line with a period 23 | 5. Use the imperative mood in the subject line 24 | 6. Wrap the body at 72 characters 25 | 7. Use the body to explain what and why vs. how 26 | 27 | Reference: [How to Write a Git Commit Message by Chris Beams](http://chris.beams.io/posts/git-commit/) 28 | 29 | ## Coding Style 30 | 31 | [PSR-2](http://www.php-fig.org/psr/psr-2/) 32 | 33 | ## Type Safety 34 | 35 | - PHP is a dynamic language and that is great for many things 36 | - BUT with great power comes great responsibility 37 | - so please use type hints where ever possible 38 | - functional methods (each, reduce, map, ...) are preferable to loops 39 | 40 | ## How to Release 41 | 42 | Bump versions in 43 | - `CHANGELOG.md` 44 | - `README.md` 45 | - `bin/dephpend` 46 | - test files 47 | 48 | Then run: 49 | ```bash 50 | # change these 51 | VERSION="MAJOR.MINOR.PATCH" 52 | COMMIT_EMAIL="valid@email.com" 53 | 54 | # copy & paste 55 | [[ -z $(git status --porcelain) ]] && \ 56 | make phar && \ 57 | mv "build/dephpend.phar" "build/dephpend-$VERSION.phar" && \ 58 | gpg --local-user "${COMMIT_EMAIL}" --detach-sign --output "build/dephpend-${VERSION}.phar.asc" "build/dephpend-${VERSION}.phar" && \ 59 | gpg --verify "build/dephpend-${VERSION}.phar.asc" "build/dephpend-${VERSION}.phar" && \ 60 | bin/prepare-tag "${VERSION}" && \ 61 | git push && \ 62 | git push origin "${VERSION}" 63 | ``` 64 | 65 | Then upload to GitHub releases and verify via `phive install dephpend` or `phive update dephpend`. 66 | 67 | The update the documentation 68 | 69 | ```bash 70 | git checkout gh-pages && \ 71 | git merge --no-edit origin/main && \ 72 | make pages && \ 73 | git commit index.html -m "Update documentation" && \ 74 | git push && \ 75 | git checkout main 76 | ``` 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.3-cli 2 | RUN mkdir -p /usr/share/man/man1 \ 3 | && apt-get update && apt-get install -y \ 4 | default-jdk \ 5 | default-jdk-headless \ 6 | && dpkg --configure -a \ 7 | && apt-get install -y \ 8 | graphviz \ 9 | plantuml 10 | COPY . /dephpend 11 | WORKDIR /dephpend 12 | RUN curl https://raw.githubusercontent.com/composer/getcomposer.org/76a7060ccb93902cd7576b67264ad91c8a2700e2/web/installer | php -- --quiet \ 13 | && php -n composer.phar install 14 | ENTRYPOINT [ "php", "-n", "-d memory_limit=-1", "./bin/dephpend" ] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2020 Michael Haeuslmann 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NO_COLOR=\x1b[0m 2 | OK_COLOR=\x1b[32;01m 3 | ERROR_COLOR=\x1b[31;01m 4 | WARN_COLOR=\x1b[33;01m 5 | 6 | PHP=php 7 | PHP_NO_INI=php -n 8 | PHPUNIT=vendor/bin/phpunit 9 | 10 | all: autoload tests testdox cov 11 | 12 | autoload: 13 | php composer.phar dumpautoload 14 | 15 | t: test 16 | test: unit feature 17 | 18 | unit: 19 | $(PHP) $(PHPUNIT) -c phpunit.xml.dist 20 | 21 | f: feature 22 | feature: 23 | @$(PHP) $(PHPUNIT) tests/feature --testdox\ 24 | | sed 's/\[x\]/$(OK_COLOR)$\[x]$(NO_COLOR)/' \ 25 | | sed -r 's/(\[ \].+)/$(ERROR_COLOR)\1$(NO_COLOR)/' \ 26 | | sed -r 's/(^[^ ].+)/$(WARN_COLOR)\1$(NO_COLOR)/' 27 | 28 | d: testdox 29 | testdox: 30 | @$(PHP_NO_INI) $(PHPUNIT) -c phpunit.xml.dist --testdox tests \ 31 | | sed 's/\[x\]/$(OK_COLOR)$\[x]$(NO_COLOR)/' \ 32 | | sed -r 's/(\[ \].+)/$(ERROR_COLOR)\1$(NO_COLOR)/' \ 33 | | sed -r 's/(^[^ ].+)/$(WARN_COLOR)\1$(NO_COLOR)/' 34 | 35 | testdox-osx: 36 | @$(PHP_NO_INI) $(PHPUNIT) -c phpunit.xml.dist --testdox tests \ 37 | | sed 's/\[x\]/$(OK_COLOR)$\[x]$(NO_COLOR)/' \ 38 | | sed -E 's/(\[ \].+)/$(ERROR_COLOR)\1$(NO_COLOR)/' \ 39 | | sed -E 's/(^[^ ].+)/$(WARN_COLOR)\1$(NO_COLOR)/' 40 | 41 | c: cov 42 | cov: 43 | @$(PHP) $(PHPUNIT) -c phpunit.xml.dist --coverage-text 44 | 45 | s: style 46 | style: 47 | @$(PHP_NO_INI) vendor/bin/php-cs-fixer fix --level=psr2 --verbose src 48 | @$(PHP_NO_INI) vendor/bin/php-cs-fixer fix --level=psr2 --verbose tests 49 | 50 | phar: 51 | @php composer.phar update --no-dev 52 | @$(PHP) box.phar build 53 | @chmod +x build/dephpend.phar 54 | @php composer.phar update 55 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /bin/check-code-cov: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This is just a quick hack to determine if the code coverage 4 | # is enough or not. 5 | # 6 | # @author Michael Haeuslmann 7 | 8 | NO_COLOR='\x1b[0m' 9 | OK_COLOR='\x1b[32;01m' 10 | ERROR_COLOR='\x1b[31;01m' 11 | WARN_COLOR='\x1b[33;01m' 12 | 13 | min_cov=95 14 | cov=$(vendor/bin/phpunit --coverage-text --colors=never \ 15 | | egrep -o "Lines:[[:space:]]+([0-9])+" \ 16 | | head -n1 \ 17 | | sed 's/Lines: *//') 18 | 19 | if [ "$cov" -eq "100" ]; then 20 | printf "${OK_COLOR}✓ Coverage is $cov%%${NO_COLOR}\n" 21 | elif [ "$cov" -ge "$min_cov" ]; then 22 | printf "${WARN_COLOR}! Coverage is only $cov%%${NO_COLOR}\n" 23 | else 24 | printf "${ERROR_COLOR}✗ Coverage is only $cov%%, but needs to be at least $min_cov%%${NO_COLOR}\n" 25 | exit 1 26 | fi 27 | 28 | -------------------------------------------------------------------------------- /bin/dephpend: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | dispatcher()); 36 | $application->add($dependencyContainer->umlCommand()); 37 | $application->add($dependencyContainer->dotCommand()); 38 | $application->add($dependencyContainer->dsmCommand()); 39 | $application->add($dependencyContainer->textCommand()); 40 | $application->add($dependencyContainer->metricsCommand()); 41 | $application->add($dependencyContainer->testFeaturesCommand()); 42 | $application->run(); 43 | -------------------------------------------------------------------------------- /bin/pdepend-xml-filter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 13 | 14 | EOT; 15 | die($doc); 16 | } 17 | 18 | exec('php -n pdepend.phar --dependency-xml='.__DIR__.'/pdepend.xml '.$argv[1]); 19 | $pdependResultArray = file(__DIR__.'/pdepend.xml', FILE_IGNORE_NEW_LINES); 20 | unlink(__DIR__.'/pdepend.xml'); 21 | 22 | $inPackage = ''; 23 | $inClass = ''; 24 | $inEfferent = false; 25 | $dependencies = []; 26 | foreach ($pdependResultArray as $line) { 27 | if (preg_match('//', $line, $matches) === 1) { 28 | $inPackage = $matches[1]; 29 | } 30 | 31 | if (preg_match('/<(?:class|interface) name="([^"]+?)">/', $line, $matches) === 1) { 32 | $inClass = $inPackage.'\\'.$matches[1]; 33 | $dependencies[$inClass] = []; 34 | } 35 | 36 | if ($inEfferent && preg_match('//', 37 | $line, $matches)) { 38 | $dependencies[$inClass][] = $matches[1].'\\'.$matches[2]; 39 | } 40 | 41 | if (strpos($line, '') !== false) { 42 | $inEfferent = true; 43 | } 44 | if (strpos($line, '') !== false) { 45 | $inEfferent = false; 46 | } 47 | } 48 | 49 | foreach ($dependencies as $from => $tos) { 50 | foreach ($tos as $to) { 51 | echo $from.' --> '.$to.PHP_EOL; 52 | } 53 | } 54 | //var_dump($dependencies); 55 | -------------------------------------------------------------------------------- /bin/php-trace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 12 | php-trace --dynamic="/path/to/tracefile.xt" 13 | 14 | EOT; 15 | exit; 16 | } 17 | 18 | if (!extension_loaded('xdebug')) { 19 | throw new LogicException('XDebug extension has not been loaded.'); 20 | } 21 | 22 | $command = PHP_BINARY 23 | .' -d xdebug.auto_trace=1' 24 | .' -d xdebug.collect_params=1' 25 | .' -d xdebug.collect_return=1' 26 | .' -d xdebug.collect_assignments=1' 27 | .' -d xdebug.trace_format=1' 28 | .' -d xdebug.trace_options=1' 29 | .customTraceOutput() 30 | .implode(' ', $argv); 31 | 32 | echo shell_exec($command); 33 | 34 | 35 | function customTraceOutput() : string 36 | { 37 | global $argv; 38 | 39 | if (strpos(implode('', $argv), '--dynamic') !== false) { 40 | $filename = preg_replace('/^.*--dynamic="?([^\s]+)"?.*$/', '$1', 41 | implode(' ', $argv)); 42 | 43 | if (!is_writable(dirname($filename))) { 44 | throw new InvalidArgumentException('Cannot write ' . basename($filename) . ' to ' . dirname($filename)); 45 | } 46 | 47 | if (preg_match('/\.xt$/', $filename) === 0) { 48 | throw new InvalidArgumentException('Filename has to use the .xt extension.'); 49 | } 50 | 51 | $argv = array_filter(array_slice($argv, 1), function (string $arg) { 52 | return strpos($arg, '--dynamic') === false; 53 | }); 54 | 55 | return ' -d xdebug.trace_output_dir="'.dirname($filename). '"' 56 | .' -d xdebug.trace_output_name="'.basename($filename, '.xt').'" '; 57 | } 58 | 59 | return ''; 60 | } 61 | -------------------------------------------------------------------------------- /bin/prepare-tag: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | # set -o xtrace 7 | 8 | declare -r __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 9 | 10 | # shellcheck source=$HOME/dotfiles/scripts/helpers 11 | source "${HOME}/dotfiles/scripts/helpers" 12 | 13 | function main() { 14 | readonly TAG="${1:-}" 15 | if [[ $TAG = "" || ! $TAG =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}$ ]]; then 16 | _error "Please provide the git tag (e.g. 2.31.2) as the first argument." 17 | exit 1 18 | fi 19 | 20 | if git show "${TAG}" >/dev/null 2>&1; then 21 | _error "Tag already exists." 22 | exit 2 23 | fi 24 | 25 | if [[ $(grep -cF "${TAG}" "${__dir}/../README.md") != 1 ]]; then 26 | _error "${TAG} not found in README.md." 27 | exit 3 28 | fi 29 | 30 | if [[ $(grep -cF "${TAG}" "${__dir}/../bin/dephpend") != 1 ]]; then 31 | _error "${TAG} not found in binary." 32 | exit 4 33 | fi 34 | 35 | if [[ $(grep -cF "${TAG}" "${__dir}/../CHANGELOG.md") != 3 ]]; then 36 | _error "${TAG} not mentioned or linked in CHANGELOG.md." 37 | exit 5 38 | fi 39 | 40 | git tag -a "${TAG}" -m "${TAG}" 41 | readonly EXIT_CODE="$?" 42 | [[ $EXIT_CODE = 0 ]] && _success "Git tag successfully created." || _error "Command 'git tag failed'." 43 | exit "${EXIT_CODE}" 44 | } 45 | 46 | main "$@" 47 | -------------------------------------------------------------------------------- /bin/qa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | = $threshold) { 99 | warning("Line coverage is only $lineCoverage% but should be 100%"); 100 | return 1; 101 | } else { 102 | error("Line coverage has to be $threshold% but is $lineCoverage"); 103 | return 1; 104 | } 105 | } 106 | 107 | /******************************************************* 108 | * Helper Functions 109 | ******************************************************/ 110 | 111 | function success(string $message) 112 | { 113 | UNIX === true 114 | ? print "\e[0K\r\x1b[32;01m✓ $message\x1b[0m".PHP_EOL 115 | : print "[x] $message".PHP_EOL; 116 | } 117 | 118 | function warning(string $message) 119 | { 120 | UNIX === true 121 | ? print "\e[0K\r\x1b[33;01m! $message\x1b[0m".PHP_EOL 122 | : print "[!] $message".PHP_EOL; 123 | } 124 | 125 | function error(string $message) 126 | { 127 | UNIX === true 128 | ? print "\e[0K\r\x1b[31;01m✗ $message\x1b[0m".PHP_EOL 129 | : print "[ ] $message".PHP_EOL; 130 | } 131 | 132 | function findPhpFilesInDir(string $dir) : array 133 | { 134 | $iterator = new \RegexIterator( 135 | new \RecursiveIteratorIterator( 136 | new \RecursiveDirectoryIterator($dir) 137 | ), '/^.+\.php$/i', \RecursiveRegexIterator::GET_MATCH 138 | ); 139 | $files = []; 140 | foreach ($iterator as $file) { 141 | $files[$file[0]] = $file[0]; 142 | } 143 | return $files; 144 | } 145 | 146 | function findPhpFilesInDirs(array $dirs) : array 147 | { 148 | return array_reduce($dirs, function (array $files, string $dir) { 149 | return array_merge($files, findPhpFilesInDir($dir)); 150 | }, []); 151 | } 152 | -------------------------------------------------------------------------------- /box.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "directories": ["src/", "vendor/"], 3 | "main": "bin/dephpend", 4 | "output": "build/dephpend.phar", 5 | "stub": true 6 | } 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dephpend/dephpend", 3 | "description": "Dependency analysis for PHP", 4 | "type": "library", 5 | "keywords": [ 6 | "dependencies", 7 | "dependency", 8 | "analysis" 9 | ], 10 | "homepage": "https://dephpend.com", 11 | "bin": ["bin/dephpend", "bin/php-trace"], 12 | "require": { 13 | "php": "^7.2", 14 | "nikic/php-parser": "^4.0", 15 | "symfony/console": "^4 || ^5", 16 | "symfony/event-dispatcher": "^4 || ^5" 17 | }, 18 | "suggest": { 19 | "ext-json": "*", 20 | "ext-mbstring": "*", 21 | "ext-tokenizer": "*" 22 | }, 23 | "require-dev": { 24 | "mikey179/vfsstream": "^1.6", 25 | "phpunit/phpunit": "^8.0", 26 | "squizlabs/php_codesniffer": "^3.3", 27 | "friendsofphp/php-cs-fixer": "^2.12" 28 | }, 29 | "minimum-stability": "stable", 30 | "prefer-stable": true, 31 | "license": "MIT", 32 | "authors": [ 33 | { 34 | "name": "Michael Haeuslmann", 35 | "email": "michael.haeuslmann@gmail.com" 36 | } 37 | ], 38 | "autoload": { 39 | "psr-4": { 40 | "Mihaeu\\PhpDependencies\\": ["src/", "tests/unit/"] 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Mihaeu\\PhpDependencies\\Tests\\Feature\\": ["tests/feature/"] 46 | }, 47 | "files": [ 48 | "vendor/phpunit/phpunit/src/Framework/Assert/Functions.php", 49 | "tests/feature/constants.php" 50 | ] 51 | }, 52 | "support": { 53 | "issues": "https://github.com/mihaeu/dephpend/issues" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /config/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # GIT PRE-COMMIT HOOK (copy to .git/hooks) 4 | # ---------------------------------------- 5 | # 6 | # - checks all changed files for PSR-2 compatibility 7 | # and aborts if any file doesn't comply 8 | # - checks code for code coverage and prints a warning 9 | # if it is not within the threshold (but doesn't aborts) 10 | 11 | PHPCS_PATH="php -n vendor/bin/php-cs-fixer" 12 | 13 | # check style of changed files 14 | git diff-index --name-only --cached --diff-filter=ACMR HEAD -- \ 15 | | xargs -I {} $PHPCS_PATH fix --dry-run --level=psr2 --verbose {} || exit 1; 16 | 17 | echo 18 | echo "Running QA ..." 19 | # code coverage will only be checked, but won't stop the commit 20 | # (it will stop pushes) 21 | bin/qa 22 | echo 23 | 24 | exit 0 25 | -------------------------------------------------------------------------------- /config/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # GIT PRE-PUSH HOOK (copy to .git/hooks) 4 | # -------------------------------------- 5 | # 6 | # - enforces code coverage to be above a certain threshold 7 | # aborts the push if threshold is not met 8 | # 9 | # (coding styles are already enforced for every commit) 10 | 11 | bin/qa 12 | exit $? 13 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Other documents 2 | 3 | - [Scope](Scope.md) 4 | -------------------------------------------------------------------------------- /doc/Scope.md: -------------------------------------------------------------------------------- 1 | # Scope 2 | 3 | Reference: [PHP documentation on variable scope](http://php.net/manual/en/language.variables.scope.php) 4 | 5 | - superglobals like `$_POST` 6 | - global scope if variable defined outside of a class or function 7 | - referenced global through `global` or `$GLOBALS` 8 | - static variables have normal scope, but don't change after the scope is left 9 | -------------------------------------------------------------------------------- /doc/dephpend-timeline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaeu/dephpend/89b1159667024246d68a89edd9531ff88ef617f5/doc/dephpend-timeline.gif -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | tests/unit 12 | 13 | 14 | 15 | 16 | src 17 | 18 | autoload.php 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Analyser/DefaultParser.php: -------------------------------------------------------------------------------- 1 | baseParser = $baseParser; 20 | } 21 | 22 | public function parse(string $code): array 23 | { 24 | return $this->baseParser->parse($code); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Analyser/Metrics.php: -------------------------------------------------------------------------------- 1 | abstractClassCount($map) 24 | + $this->interfaceCount($map) 25 | + $this->traitCount($map); 26 | if ($abstractions === 0) { 27 | return 0.0; 28 | } 29 | $allClasses = $abstractions + $this->classCount($map); 30 | return $abstractions / $allClasses; 31 | } 32 | 33 | public function classCount(DependencyMap $map) : int 34 | { 35 | return $this->countFilteredItems($map, function (Dependency $dependency) { 36 | return $dependency instanceof Clazz; 37 | }); 38 | } 39 | 40 | public function abstractClassCount(DependencyMap $map) : int 41 | { 42 | return $this->countFilteredItems($map, function (Dependency $dependency) { 43 | return $dependency instanceof AbstractClazz; 44 | }); 45 | } 46 | 47 | public function interfaceCount(DependencyMap $map) : int 48 | { 49 | return $this->countFilteredItems($map, function (Dependency $dependency) { 50 | return $dependency instanceof Interfaze; 51 | }); 52 | } 53 | 54 | public function traitCount(DependencyMap $map) : int 55 | { 56 | return $this->countFilteredItems($map, function (Dependency $dependency) { 57 | return $dependency instanceof Trait_; 58 | }); 59 | } 60 | 61 | /** 62 | * Afferent coupling is an indicator for the responsibility of a package. 63 | * 64 | * @param DependencyMap $map 65 | * 66 | * @return array 67 | */ 68 | public function afferentCoupling(DependencyMap $map) : array 69 | { 70 | return $map->fromDependencies()->reduce([], function (array $afferent, Dependency $from) use ($map) { 71 | $afferent[$from->toString()] = $map->reduce( 72 | 0, 73 | function (int $count, Dependency $fromOther, Dependency $to) use ($from): int { 74 | return $from->equals($to) 75 | ? $count + 1 76 | : $count; 77 | } 78 | ); 79 | return $afferent; 80 | }); 81 | } 82 | 83 | /** 84 | * Efferent coupling is an indicator for how independent a package is. 85 | * 86 | * @param DependencyMap $map 87 | * 88 | * @return array 89 | */ 90 | public function efferentCoupling(DependencyMap $map) : array 91 | { 92 | return $map->reduce([], function (array $efferent, Dependency $from, Dependency $to) use ($map) { 93 | $efferent[$from->toString()] = $map->get($from)->count(); 94 | return $efferent; 95 | }); 96 | } 97 | 98 | /** 99 | * Instability is an indicator for how resilient a package is towards change. 100 | * 101 | * @param DependencyMap $map 102 | * 103 | * @return array Key: Class Value: Range from 0 (completely stable) to 1 (completely unstable) 104 | */ 105 | public function instability(DependencyMap $map) : array 106 | { 107 | $ce = $this->efferentCoupling($map); 108 | $ca = $this->afferentCoupling($map); 109 | $instability = []; 110 | foreach ($ce as $class => $count) { 111 | $totalCoupling = $ce[$class] + $ca[$class]; 112 | $instability[$class] = $ce[$class] / $totalCoupling; 113 | } 114 | return $instability; 115 | } 116 | 117 | private function countFilteredItems(DependencyMap $map, \Closure $closure) 118 | { 119 | return $map->fromDependencies()->filter($closure)->count(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Analyser/Parser.php: -------------------------------------------------------------------------------- 1 | dependencyInspectionVisitor = $dependencyInspectionVisitor; 36 | 37 | $this->nodeTraverser = $nodeTraverser; 38 | $this->nodeTraverser->addVisitor(new NameResolver()); 39 | $this->nodeTraverser->addVisitor($this->dependencyInspectionVisitor); 40 | 41 | $this->parser = $parser; 42 | } 43 | 44 | public function analyse(PhpFileSet $files) : DependencyMap 45 | { 46 | $files->each(function (PhpFile $file) { 47 | try { 48 | $this->nodeTraverser->traverse( 49 | $this->parser->parse($file->code()) 50 | ); 51 | } catch (Error $e) { 52 | throw new ParserException($e->getMessage(), $file->file()->getPathname()); 53 | } 54 | }); 55 | 56 | return $this->dependencyInspectionVisitor->dependencies(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Analyser/XDebugFunctionTraceAnalyser.php: -------------------------------------------------------------------------------- 1 | dependencyFactory = $dependencyFactory; 29 | } 30 | 31 | public function analyse(\SplFileInfo $file) : DependencyMap 32 | { 33 | $fileHandle = @fopen($file->getPathname(), 'r'); 34 | if (!$fileHandle) { 35 | throw new \InvalidArgumentException('Unable to open trace file for reading'); 36 | } 37 | 38 | $line = fgets($fileHandle); 39 | $dependencies = new DependencyMap(); 40 | while ($line !== false) { 41 | $dependencies = $this->extractDependenciesFromLine($dependencies, $line); 42 | $line = fgets($fileHandle); 43 | } 44 | fclose($fileHandle); 45 | return $dependencies; 46 | } 47 | 48 | private function extractDependenciesFromLine(DependencyMap $dependencies, string $line) : DependencyMap 49 | { 50 | $tokens = $this->extractFields($line); 51 | if ($this->isNotMethodEntryTrace($tokens) 52 | || $this->containsOnlyScalarValues($tokens) 53 | || $this->isGlobalFunction($tokens)) { 54 | return $dependencies; 55 | } 56 | return $dependencies->addSet( 57 | $this->extractFromClass($tokens), 58 | $this->extractToSet($tokens) 59 | ); 60 | } 61 | 62 | private function isNotMethodEntryTrace(array $tokens) : bool 63 | { 64 | return count($tokens) <= self::PARAMETER_START_INDEX; 65 | } 66 | 67 | private function extractFields(string $line) : array 68 | { 69 | return explode("\t", str_replace("\n", '', $line)); 70 | } 71 | 72 | private function containsOnlyScalarValues(array $tokens) : bool 73 | { 74 | return strpos(implode('', array_slice($tokens, self::PARAMETER_START_INDEX)), 'class ') === false; 75 | } 76 | 77 | private function isGlobalFunction(array $tokens) : bool 78 | { 79 | return strpos($tokens[self::FUNCTION_NAME_INDEX], '->') === false 80 | && strpos($tokens[self::FUNCTION_NAME_INDEX], '::') === false; 81 | } 82 | 83 | private function extractFromClass(array $tokens) : Clazz 84 | { 85 | $classWithoutMethod = preg_split('/(->)|(::)/', $tokens[self::FUNCTION_NAME_INDEX]); 86 | $classParts = explode("\\", $classWithoutMethod[0]); 87 | return $this->dependencyFactory->createClazzFromStringArray($classParts); 88 | } 89 | 90 | private function extractToSet(array $tokens) : DependencySet 91 | { 92 | return array_reduce(array_slice($tokens, self::PARAMETER_START_INDEX), function (DependencySet $set, string $token) { 93 | if (strpos($token, 'class') === false) { 94 | return $set; 95 | } 96 | 97 | $classParts = explode('\\', str_replace('class ', '', $token)); 98 | return $set->add($this->dependencyFactory->createClazzFromStringArray($classParts)); 99 | }, new DependencySet()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Cli/Application.php: -------------------------------------------------------------------------------- 1 | setHelperSet($this->getDefaultHelperSet()); 25 | $this->setDefaultCommand('list'); 26 | $this->setDispatcher($dispatcher); 27 | } 28 | 29 | public function setErrorOutput(ErrorOutput $errorOutput): void 30 | { 31 | if (!$this->errorOutput) { 32 | $this->errorOutput = $errorOutput; 33 | } 34 | } 35 | 36 | /** 37 | * Commands are added here instead of before executing run(), because 38 | * we need access to command line options in order to inject the 39 | * right dependencies. 40 | * 41 | * @param InputInterface $input 42 | * @param OutputInterface $output 43 | * 44 | * @return int 45 | * 46 | * @throws \Symfony\Component\Console\Exception\LogicException 47 | */ 48 | public function doRun(InputInterface $input, OutputInterface $output) 49 | { 50 | $this->printWarningIfXdebugIsEnabled($input, $output); 51 | 52 | try { 53 | parent::doRun($input, $output); 54 | } catch (ParserException $e) { 55 | $this->writeToStdErr($input, $output, 'Sorry, we could not analyse your dependencies, ' 56 | . 'because the sources contain syntax errors:' . PHP_EOL . PHP_EOL 57 | . $e->getMessage() . ' in file ' . $e->getFile() . ''); 58 | return $e->getCode() ?? 1; 59 | } catch (\Throwable $e) { 60 | if ($output !== null) { 61 | $this->writeToStdErr( 62 | $input, 63 | $output, 64 | "Something went wrong, this shouldn't happen." 65 | . ' Please take a minute and report this issue:' 66 | . ' https://github.com/mihaeu/dephpend/issues' 67 | . PHP_EOL . PHP_EOL 68 | . $e->getMessage() 69 | . PHP_EOL . PHP_EOL 70 | . "[{$e->getFile()} at line {$e->getLine()}]" 71 | ); 72 | } 73 | return $e->getCode() ?? 1; 74 | } 75 | 76 | return 0; 77 | } 78 | 79 | private function printWarningIfXdebugIsEnabled(InputInterface $input, OutputInterface $output): void 80 | { 81 | if (\extension_loaded('xdebug')) { 82 | $this->writeToStdErr($input, $output, '' . self::XDEBUG_WARNING . ''); 83 | } 84 | } 85 | 86 | private function writeToStdErr(InputInterface $input, OutputInterface $output, string $message): void 87 | { 88 | $this->setErrorOutput(new ErrorOutput(new SymfonyStyle($input, $output))); 89 | $this->errorOutput->writeln($message); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Cli/BaseCommand.php: -------------------------------------------------------------------------------- 1 | dependencies = new DependencyMap(); 32 | $this->postProcessors = Functional::id(); 33 | } 34 | 35 | 36 | public function setDependencies(DependencyMap $dependencies) 37 | { 38 | $this->dependencies = $dependencies; 39 | } 40 | 41 | public function setPostProcessors(\Closure $postProcessors) 42 | { 43 | $this->postProcessors = $postProcessors; 44 | } 45 | 46 | protected function configure() 47 | { 48 | parent::configure(); 49 | $this 50 | ->addArgument( 51 | 'source', 52 | InputArgument::IS_ARRAY | InputArgument::REQUIRED, 53 | 'Location of your PHP source files.' 54 | ) 55 | ->addOption( 56 | 'internals', 57 | null, 58 | InputOption::VALUE_NONE, 59 | 'Check for dependencies from internal PHP Classes like SplFileInfo.' 60 | ) 61 | ->addOption( 62 | 'depth', 63 | 'd', 64 | InputOption::VALUE_OPTIONAL, 65 | 'Output dependencies as packages instead of single classes.', 66 | 0 67 | ) 68 | ->addOption( 69 | 'underscore-namespaces', 70 | null, 71 | InputOption::VALUE_NONE, 72 | 'Parse underscores in Class names as namespaces.' 73 | ) 74 | ->addOption( 75 | 'filter-namespace', 76 | null, 77 | InputOption::VALUE_REQUIRED, 78 | 'Analyse only classes where both to and from are in this namespace.' 79 | ) 80 | ->addOption( 81 | 'filter-from', 82 | 'f', 83 | InputOption::VALUE_REQUIRED, 84 | 'Analyse only dependencies which originate from this namespace.' 85 | ) 86 | ->addOption( 87 | 'no-classes', 88 | null, 89 | InputOption::VALUE_NONE, 90 | 'Remove all classes and analyse only namespaces.' 91 | ) 92 | ->addOption( 93 | 'exclude-regex', 94 | 'e', 95 | InputOption::VALUE_REQUIRED, 96 | 'Exclude all dependencies which match the (PREG) regular expression.' 97 | ) 98 | ->addOption( 99 | 'dynamic', 100 | null, 101 | InputOption::VALUE_REQUIRED, 102 | 'Adds dependency information from dynamically analysed function traces, for more information check out https://dephpend.com' 103 | ) 104 | ; 105 | } 106 | 107 | /** 108 | * @param string $destination 109 | * 110 | * @throws \Exception 111 | */ 112 | protected function ensureOutputFormatIsValid(string $destination) 113 | { 114 | if (!in_array(preg_replace('/.+\.(\w+)$/', '$1', $destination), $this->allowedFormats, true)) { 115 | throw new \InvalidArgumentException('Output format is not allowed ('.implode(', ', $this->allowedFormats).')'); 116 | } 117 | } 118 | 119 | /** 120 | * @param string[] $sources 121 | * 122 | * @throws \InvalidArgumentException 123 | */ 124 | protected function ensureSourcesAreReadable(array $sources) 125 | { 126 | foreach ($sources as $source) { 127 | if (!is_readable($source)) { 128 | throw new \InvalidArgumentException('File/Directory does not exist or is not readable.'); 129 | } 130 | } 131 | } 132 | 133 | /** 134 | * @param string $destination 135 | * 136 | * @throws \Exception 137 | */ 138 | protected function ensureDestinationIsWritable(string $destination) 139 | { 140 | if (!is_writable(dirname($destination))) { 141 | throw new \InvalidArgumentException('Destination is not writable.'); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Cli/Dispatcher.php: -------------------------------------------------------------------------------- 1 | staticAnalyser = $staticAnalyser; 42 | $this->xDebugFunctionTraceAnalyser = $xDebugFunctionTraceAnalyser; 43 | $this->phpFileFinder = $phpFileFinder; 44 | $this->dependencyFilter = $dependencyFilter; 45 | } 46 | 47 | public function dispatch($event, string $eventName = null): object 48 | { 49 | $event = parent::dispatch($event, $eventName); 50 | 51 | if ($eventName !== ConsoleEvents::COMMAND 52 | || !($event instanceof ConsoleEvent) 53 | ) { 54 | return $event; 55 | } 56 | 57 | $command = $event->getCommand(); 58 | if ($command instanceof BaseCommand) { 59 | $dependencies = $this->analyzeDependencies($event->getInput()); 60 | $postProcessors = $this->getPostProcessors($event->getInput()); 61 | $command->setDependencies($dependencies); 62 | $command->setPostProcessors($postProcessors); 63 | } 64 | 65 | return $event; 66 | } 67 | 68 | 69 | /** 70 | * @param InputInterface $input 71 | * @param DependencyContainer $dependencyContainer 72 | * 73 | * @return DependencyMap 74 | * 75 | * @throws \LogicException 76 | */ 77 | private function analyzeDependencies(InputInterface $input): DependencyMap 78 | { 79 | // run static analysis 80 | $dependencies = $this->staticAnalyser->analyse( 81 | $this->phpFileFinder->getAllPhpFilesFromSources($input->getArgument('source')) 82 | ); 83 | 84 | // optional: analyse results of dynamic analysis and merge 85 | if ($input->getOption('dynamic')) { 86 | $traceFile = new \SplFileInfo($input->getOption('dynamic')); 87 | $dependencies = $dependencies->addMap( 88 | $this->xDebugFunctionTraceAnalyser->analyse($traceFile) 89 | ); 90 | } 91 | 92 | // apply pre-filters 93 | return $this->dependencyFilter->filterByOptions($dependencies, $input->getOptions()); 94 | } 95 | 96 | private function getPostProcessors(InputInterface $input): \Closure 97 | { 98 | return $this->dependencyFilter->postFiltersByOptions($input->getOptions()); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Cli/DotCommand.php: -------------------------------------------------------------------------------- 1 | dotWrapper = $dotWrapper; 22 | 23 | $this->defaultFormat = 'png'; 24 | $this->allowedFormats = [$this->defaultFormat, 'dot', 'svg']; 25 | } 26 | 27 | protected function configure() 28 | { 29 | parent::configure(); 30 | 31 | $this 32 | ->setDescription('Generate a dot graph of your dependencies') 33 | ->addOption( 34 | 'output', 35 | 'o', 36 | InputOption::VALUE_REQUIRED, 37 | 'Destination for the generated dot graph (in '.implode('/', [$this->defaultFormat, 'svg']).' format).' 38 | ) 39 | ->addOption( 40 | 'keep', 41 | null, 42 | InputOption::VALUE_NONE, 43 | 'Keep the intermediate dot file instead of deleting it.' 44 | ) 45 | ; 46 | } 47 | 48 | protected function execute(InputInterface $input, OutputInterface $output) 49 | { 50 | $options = $input->getOptions(); 51 | $this->ensureDestinationIsWritable($options['output']); 52 | $this->ensureOutputFormatIsValid($options['output']); 53 | 54 | $this->dotWrapper->generate( 55 | $this->dependencies->reduceEachDependency($this->postProcessors), 56 | new \SplFileInfo($options['output']), 57 | $options['keep'] 58 | ); 59 | 60 | return 0; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Cli/DsmCommand.php: -------------------------------------------------------------------------------- 1 | defaultFormat = 'html'; 22 | $this->allowedFormats = [$this->defaultFormat]; 23 | 24 | $this->dependencyStructureMatrixHtmlFormatter = $dependencyStructureMatrixFormatter; 25 | } 26 | 27 | protected function configure() 28 | { 29 | parent::configure(); 30 | 31 | $this 32 | ->setDescription('Generate a Dependency Structure Matrix of your dependencies') 33 | ->addOption( 34 | 'format', 35 | null, 36 | InputOption::VALUE_REQUIRED, 37 | 'Output format.', 38 | 'html' 39 | ) 40 | ; 41 | } 42 | 43 | protected function execute(InputInterface $input, OutputInterface $output) 44 | { 45 | $options = $input->getOptions(); 46 | $this->ensureSourcesAreReadable($input->getArgument('source')); 47 | $this->ensureOutputFormatIsValid($options['format']); 48 | 49 | $output->write($this->dependencyStructureMatrixHtmlFormatter->format( 50 | $this->dependencies, 51 | $this->postProcessors 52 | )); 53 | 54 | return 0; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Cli/ErrorOutput.php: -------------------------------------------------------------------------------- 1 | symfonyStyle = $symfonyStyle; 17 | } 18 | 19 | public function writeln(string $message): void 20 | { 21 | $this->symfonyStyle->getErrorStyle()->writeln($message); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Cli/MetricsCommand.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 20 | parent::__construct('metrics'); 21 | } 22 | 23 | protected function configure() 24 | { 25 | parent::configure(); 26 | 27 | $this 28 | ->setDescription('Generate dependency metrics') 29 | ; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | * 35 | * @throws \Symfony\Component\Console\Exception\InvalidArgumentException 36 | */ 37 | protected function execute(InputInterface $input, OutputInterface $output) 38 | { 39 | $table = new Table($output); 40 | $table->setRows([ 41 | ['Classes: ', $this->metrics->classCount($this->dependencies)], 42 | ['Abstract classes: ', $this->metrics->abstractClassCount($this->dependencies)], 43 | ['Interfaces: ', $this->metrics->interfaceCount($this->dependencies)], 44 | ['Traits: ', $this->metrics->traitCount($this->dependencies)], 45 | ['Abstractness: ', sprintf('%.3f', $this->metrics->abstractness($this->dependencies))], 46 | ]); 47 | $table->render(); 48 | 49 | $table = new Table($output); 50 | $table->setHeaders(['', 'Afferent Coupling', 'Efferent Coupling', 'Instability']); 51 | $table->setRows($this->combineMetrics( 52 | $this->metrics->afferentCoupling($this->dependencies), 53 | $this->metrics->efferentCoupling($this->dependencies), 54 | $this->metrics->instability($this->dependencies) 55 | )); 56 | $table->render(); 57 | 58 | return 0; 59 | } 60 | 61 | private function combineMetrics(array $afferentCoupling, array $efferentCoupling, array $instability) : array 62 | { 63 | $result = []; 64 | foreach ($afferentCoupling as $className => $afferentCouplingValue) { 65 | $result[] = [ 66 | $className, 67 | $afferentCouplingValue, 68 | $efferentCoupling[$className], 69 | sprintf('%.2f', $instability[$className]) 70 | ]; 71 | } 72 | return $result; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Cli/TestFeaturesCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Test support for dependency detection') 31 | ; 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | protected function execute(InputInterface $input, OutputInterface $output) 38 | { 39 | $files = $this->fetchAllFeatureTests(); 40 | $results = $this->runAllTests($files); 41 | 42 | usort($results, $this->sortSuccessFirst()); 43 | foreach ($results as $result) { 44 | $output->writeln($result[1]); 45 | } 46 | return 0; 47 | } 48 | 49 | public function runTest(string $filename) : array 50 | { 51 | $_SERVER['argv'] = [0, 'text', $filename]; 52 | $application = new Application('', '', (new DependencyContainer([]))->dispatcher()); 53 | $application->setAutoExit(false); 54 | $applicationOutput = new BufferedOutput(); 55 | $application->doRun(new ArgvInput($_SERVER['argv']), $applicationOutput); 56 | 57 | $expected = $this->getExpectations($filename); 58 | $actual = $this->cleanOutput($applicationOutput->fetch()); 59 | return $expected === $actual 60 | ? [true, '[✓] '.$this->extractFeatureName($filename).''] 61 | : [false, '[✗] '.$this->extractFeatureName($filename).'']; 62 | } 63 | 64 | private function cleanOutput(string $output) : string 65 | { 66 | return trim(str_replace(Application::XDEBUG_WARNING, '', $output)); 67 | } 68 | 69 | /** 70 | * @param string $filename 71 | * 72 | * @return string 73 | */ 74 | private function extractFeatureName(string $filename) : string 75 | { 76 | preg_match_all('/((?:^|[A-Z])[a-z0-9]+)/', $filename, $matches); 77 | return str_replace(['cli', ' feature'], '', strtolower(implode(' ', $matches[1]))); 78 | } 79 | 80 | /** 81 | * @param string $filename 82 | * 83 | * @return string 84 | */ 85 | private function getExpectations(string $filename) : string 86 | { 87 | $expectations = []; 88 | foreach (file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { 89 | if (strpos($line, '# ') !== 0) { 90 | break; 91 | } 92 | $expectations[] = str_replace('# ', '', $line); 93 | } 94 | return implode(PHP_EOL, $expectations); 95 | } 96 | 97 | /** 98 | * 99 | * @return \RegexIterator 100 | */ 101 | protected function fetchAllFeatureTests() : \RegexIterator 102 | { 103 | return new \RegexIterator( 104 | new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__ . '/../../tests/samples')), 105 | '/^.+Feature\.php$/i', 106 | \RecursiveRegexIterator::GET_MATCH 107 | ); 108 | } 109 | 110 | /** 111 | * @param $files 112 | * 113 | * @return array 114 | */ 115 | protected function runAllTests($files) : array 116 | { 117 | return array_map(function ($filename) { 118 | return $this->runTest($filename[0]); 119 | }, iterator_to_array($files)); 120 | } 121 | 122 | private function sortSuccessFirst() : \Closure 123 | { 124 | return function (array $x, array $y) { 125 | if ($x[0] === true) { 126 | return -1; 127 | } elseif ($y[0] === true) { 128 | return 1; 129 | } 130 | return 0; 131 | }; 132 | } 133 | } 134 | // @codeCoverageIgnoreEnd 135 | -------------------------------------------------------------------------------- /src/Cli/TextCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Prints a list of all dependencies'); 22 | } 23 | 24 | protected function execute(InputInterface $input, OutputInterface $output) 25 | { 26 | $this->ensureSourcesAreReadable($input->getArgument('source')); 27 | 28 | $output->writeln($this->dependencies->reduceEachDependency($this->postProcessors)->toString()); 29 | 30 | return 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Cli/UmlCommand.php: -------------------------------------------------------------------------------- 1 | plantUmlWrapper = $plantUmlWrapper; 22 | 23 | $this->defaultFormat = 'png'; 24 | $this->allowedFormats = [$this->defaultFormat]; 25 | } 26 | 27 | protected function configure() 28 | { 29 | parent::configure(); 30 | 31 | $this 32 | ->setDescription('Generate a UML Class diagram of your dependencies') 33 | ->addOption( 34 | 'output', 35 | 'o', 36 | InputOption::VALUE_REQUIRED, 37 | 'Destination for the generated class diagram (in .png format).' 38 | ) 39 | ->addOption( 40 | 'keep-uml', 41 | null, 42 | InputOption::VALUE_NONE, 43 | 'Keep the intermediate PlantUML file instead of deleting it.' 44 | ) 45 | ; 46 | } 47 | 48 | protected function execute(InputInterface $input, OutputInterface $output) 49 | { 50 | $options = $input->getOptions(); 51 | $this->ensureSourcesAreReadable($input->getArgument('source')); 52 | $this->ensureOutputExists($options['output']); 53 | $this->ensureDestinationIsWritable($options['output']); 54 | $this->ensureOutputFormatIsValid($options['output']); 55 | 56 | $destination = new \SplFileInfo($options['output']); 57 | $this->plantUmlWrapper->generate( 58 | $this->dependencies->reduceEachDependency($this->postProcessors), 59 | $destination, 60 | $options['keep-uml'] ?? false 61 | ); 62 | 63 | return 0; 64 | } 65 | 66 | /** 67 | * @param $outputOption 68 | * 69 | * @throws \InvalidArgumentException 70 | */ 71 | private function ensureOutputExists($outputOption) 72 | { 73 | if ($outputOption === null) { 74 | throw new \InvalidArgumentException('Output not defined (use "help" for more information).'); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Dependencies/AbstractClazz.php: -------------------------------------------------------------------------------- 1 | ensureClassNameIsValid($name); 22 | 23 | $this->name = $name; 24 | if ($clazzNamespace === null) { 25 | $clazzNamespace = new Namespaze([]); 26 | } 27 | $this->namespaze = $clazzNamespace; 28 | } 29 | 30 | public function equals(Dependency $other) : bool 31 | { 32 | return $this->toString() === $other->toString(); 33 | } 34 | 35 | public function toString() : string 36 | { 37 | return $this->hasNamespace() 38 | ? $this->namespaze.'\\'.$this->name 39 | : $this->name; 40 | } 41 | 42 | public function __toString() : string 43 | { 44 | return $this->toString(); 45 | } 46 | 47 | public function namespaze() : Namespaze 48 | { 49 | return $this->namespaze; 50 | } 51 | 52 | public function hasNamespace() : bool 53 | { 54 | return $this->namespaze->toString() !== ''; 55 | } 56 | 57 | public function count() : int 58 | { 59 | return 1 + $this->namespaze->count(); 60 | } 61 | 62 | public function reduceToDepth(int $maxDepth) : Dependency 63 | { 64 | return $this->count() <= $maxDepth || $maxDepth === 0 65 | ? $this 66 | : $this->namespaze->reduceToDepth($maxDepth); 67 | } 68 | 69 | public function reduceDepthFromLeftBy(int $reduction) : Dependency 70 | { 71 | return $this->count() <= $reduction || $reduction === 0 72 | ? $this 73 | : new Clazz($this->name, $this->namespaze->reduceDepthFromLeftBy($reduction)); 74 | } 75 | 76 | /** 77 | * @param string $name 78 | * 79 | * @throws \InvalidArgumentException 80 | */ 81 | private function ensureClassNameIsValid(string $name) 82 | { 83 | if (preg_match('/^[a-zA-Z0-9_\x7f-\xff]+$/u', $name) !== 1) { 84 | throw new \InvalidArgumentException('Class name "' . $name . '" is not valid.'); 85 | } 86 | } 87 | 88 | public function inNamespaze(Namespaze $other) : bool 89 | { 90 | return $this->namespaze->inNamespaze($other); 91 | } 92 | 93 | public function isNamespaced() : bool 94 | { 95 | return $this->namespaze->count() > 0; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Dependencies/Dependency.php: -------------------------------------------------------------------------------- 1 | extractClazzPart($parts), 19 | new Namespaze($this->extractNamespaceParts($parts)) 20 | ); 21 | } catch (\InvalidArgumentException $exception) { 22 | $clazz = new NullDependency(); 23 | } 24 | return $clazz; 25 | } 26 | 27 | /** 28 | * @param array $parts 29 | * 30 | * @return AbstractClazz 31 | */ 32 | final public function createAbstractClazzFromStringArray(array $parts) : AbstractClazz 33 | { 34 | return new AbstractClazz( 35 | $this->extractClazzPart($parts), 36 | new Namespaze($this->extractNamespaceParts($parts)) 37 | ); 38 | } 39 | 40 | /** 41 | * @param array $parts 42 | * 43 | * @return Interfaze 44 | */ 45 | final public function createInterfazeFromStringArray(array $parts) : Interfaze 46 | { 47 | return new Interfaze( 48 | $this->extractClazzPart($parts), 49 | new Namespaze($this->extractNamespaceParts($parts)) 50 | ); 51 | } 52 | 53 | /** 54 | * @param array $parts 55 | * 56 | * @return Trait_ 57 | */ 58 | final public function createTraitFromStringArray(array $parts) : Trait_ 59 | { 60 | return new Trait_( 61 | $this->extractClazzPart($parts), 62 | new Namespaze($this->extractNamespaceParts($parts)) 63 | ); 64 | } 65 | 66 | /** 67 | * @param array $parts 68 | * 69 | * @return array 70 | */ 71 | protected function extractNamespaceParts(array $parts) 72 | { 73 | return array_map(function (string $part) { 74 | return trim($part); 75 | }, array_slice($parts, 0, -1)); 76 | } 77 | 78 | /** 79 | * @param array $parts 80 | * 81 | * @return mixed 82 | */ 83 | protected function extractClazzPart(array $parts) 84 | { 85 | return trim(array_slice($parts, -1)[0]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Dependencies/DependencyFilter.php: -------------------------------------------------------------------------------- 1 | internals = $internals; 20 | } 21 | 22 | public function filterByOptions(DependencyMap $dependencies, array $options) : DependencyMap 23 | { 24 | if (isset($options['underscore-namespaces']) && $options['underscore-namespaces'] === true) { 25 | $dependencies = $this->mapNamespaces($dependencies); 26 | } 27 | 28 | if (!$options['internals']) { 29 | $dependencies = $this->removeInternals($dependencies); 30 | } 31 | 32 | if (isset($options['filter-from'])) { 33 | $dependencies = $this->filterByFromNamespace($dependencies, $options['filter-from']); 34 | } 35 | 36 | if ($options['filter-namespace']) { 37 | $dependencies = $this->filterByNamespace($dependencies, $options['filter-namespace']); 38 | } 39 | 40 | if (isset($options['exclude-regex'])) { 41 | $this->ensureRegexIsValid($options['exclude-regex']); 42 | $dependencies = $this->excludeByRegex($dependencies, $options['exclude-regex']); 43 | } 44 | 45 | return $dependencies; 46 | } 47 | 48 | public function postFiltersByOptions(array $options) : \Closure 49 | { 50 | $filters = []; 51 | if ($options['depth'] > 0) { 52 | $filters[] = $this->reduceDependencyByDepth((int) $options['depth']); 53 | } 54 | 55 | if (isset($options['no-classes']) && $options['no-classes'] === true) { 56 | $filters[] = $this->reduceDependencyToNamespace(); 57 | } 58 | return Functional::compose(...$filters); 59 | } 60 | 61 | public function removeInternals(DependencyMap $dependencies) : DependencyMap 62 | { 63 | return $dependencies->reduce(new DependencyMap(), function (DependencyMap $map, Dependency $from, Dependency $to) { 64 | return !in_array($to->toString(), $this->internals, true) 65 | ? $map->add($from, $to) 66 | : $map; 67 | }); 68 | } 69 | 70 | public function filterByNamespace(DependencyMap $dependencies, string $namespace) : DependencyMap 71 | { 72 | $namespace = new Namespaze(array_filter(explode('\\', $namespace))); 73 | return $dependencies->reduce(new DependencyMap(), $this->filterNamespaceFn($namespace)); 74 | } 75 | 76 | public function excludeByRegex(DependencyMap $dependencies, string $regex) : DependencyMap 77 | { 78 | return $dependencies->reduce(new DependencyMap(), function (DependencyMap $map, Dependency $from, Dependency $to) use ($regex) { 79 | return preg_match($regex, $from->toString()) === 1 || preg_match($regex, $to->toString()) === 1 80 | ? $map 81 | : $map->add($from, $to); 82 | }); 83 | } 84 | 85 | public function filterByFromNamespace(DependencyMap $dependencies, string $namespace) : DependencyMap 86 | { 87 | $namespace = new Namespaze(array_filter(explode('\\', $namespace))); 88 | return $dependencies->reduce(new DependencyMap(), function (DependencyMap $map, Dependency $from, Dependency $to) use ($namespace) { 89 | return $from->inNamespaze($namespace) 90 | ? $map->add($from, $to) 91 | : $map; 92 | }); 93 | } 94 | 95 | private function filterNamespaceFn(Namespaze $namespaze) : \Closure 96 | { 97 | return function (DependencyMap $map, Dependency $from, Dependency $to) use ($namespaze) : DependencyMap { 98 | return $from->inNamespaze($namespaze) && $to->inNamespaze($namespaze) 99 | ? $map->add($from, $to) 100 | : $map; 101 | }; 102 | } 103 | 104 | public function filterByDepth(DependencyMap $dependencies, int $depth) : DependencyMap 105 | { 106 | if ($depth === 0) { 107 | return clone $dependencies; 108 | } 109 | 110 | return $dependencies->reduce(new DependencyMap(), function (DependencyMap $dependencies, Dependency $from, Dependency $to) use ($depth) { 111 | return $dependencies->add( 112 | $from->reduceToDepth($depth), 113 | $to->reduceToDepth($depth) 114 | ); 115 | }); 116 | } 117 | 118 | public function filterClasses(DependencyMap $dependencies) : DependencyMap 119 | { 120 | return $dependencies->reduce(new DependencyMap(), function (DependencyMap $map, Dependency $from, Dependency $to) { 121 | if ($from->namespaze()->count() === 0 || $to->namespaze()->count() === 0) { 122 | return $map; 123 | } 124 | return $map->add($from->namespaze(), $to->namespaze()); 125 | }); 126 | } 127 | 128 | public function reduceDependencyToNamespace() : \Closure 129 | { 130 | return function (Dependency $dependency) : Dependency { 131 | return $dependency->namespaze(); 132 | }; 133 | } 134 | 135 | public function reduceDependencyByDepth(int $depth) : \Closure 136 | { 137 | return function (Dependency $dependency) use ($depth) : Dependency { 138 | return $dependency->reduceToDepth($depth); 139 | }; 140 | } 141 | 142 | public function mapNamespaces(DependencyMap $dependencies) : DependencyMap 143 | { 144 | return $dependencies->reduceEachDependency(function (Dependency $dependency) { 145 | if (strpos($dependency->toString(), '_') === false) { 146 | return $dependency; 147 | } 148 | if ($dependency->isNamespaced()) { 149 | return $dependency; 150 | } 151 | $parts = explode('_', $dependency->toString()); 152 | return new Clazz($parts[count($parts) - 1], new Namespaze(array_slice($parts, 0, -1))); 153 | }); 154 | } 155 | 156 | private function ensureRegexIsValid(string $regex) 157 | { 158 | if (@preg_match($regex, '') === false) { 159 | throw new \InvalidArgumentException( 160 | 'Regular expression ('.$regex.') is not valid.' 161 | ); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Dependencies/DependencyMap.php: -------------------------------------------------------------------------------- 1 | equals($to) 21 | || $from->count() === 0 22 | || $to->count() === 0 23 | || \in_array($to->toString(), ['self', 'parent', 'static'])) { 24 | return $clone; 25 | } 26 | 27 | if (array_key_exists($from->toString(), $clone->map)) { 28 | $clone->map[$from->toString()][self::$VALUE] = $clone->map[$from->toString()][self::$VALUE]->add($to); 29 | } else { 30 | $clone->map[$from->toString()] = [ 31 | self::$KEY => $from, 32 | self::$VALUE => (new DependencySet())->add($to), 33 | ]; 34 | } 35 | return $clone; 36 | } 37 | 38 | public function addMap(self $other) : self 39 | { 40 | return $this->reduce($other, function (DependencyMap $map, Dependency $from, Dependency $to) { 41 | return $map->add($from, $to); 42 | }); 43 | } 44 | 45 | public function addSet(Dependency $from, DependencySet $toSet) : self 46 | { 47 | $clone = $toSet->reduce($this, function (DependencyMap $map, Dependency $to) use ($from) { 48 | return $map->add($from, $to); 49 | }); 50 | return $clone; 51 | } 52 | 53 | public function get(Dependency $from) : DependencySet 54 | { 55 | return $this->map[$from->toString()][self::$VALUE]; 56 | } 57 | 58 | public function fromDependencies() : DependencySet 59 | { 60 | return $this->reduce(new DependencySet(), function (DependencySet $set, Dependency $from) { 61 | return $set->add($from); 62 | }); 63 | } 64 | 65 | public function allDependencies() : DependencySet 66 | { 67 | return $this->reduce(new DependencySet(), function (DependencySet $set, Dependency $from, Dependency $to) { 68 | return $set 69 | ->add($from) 70 | ->add($to); 71 | }); 72 | } 73 | 74 | public function mapAllDependencies(\Closure $mappers) : DependencySet 75 | { 76 | return $this->reduce(new DependencySet(), function (DependencySet $set, Dependency $from, Dependency $to) use ($mappers) { 77 | return $set 78 | ->add($mappers($from)) 79 | ->add($mappers($to)); 80 | }); 81 | } 82 | 83 | /** 84 | * This variant of reduce takes a \Closure which takes only a single Dependency 85 | * (as opposed to a pair of $to and $from) and applies it to both $to and $from. 86 | * 87 | * @param \Closure $mappers 88 | * 89 | * @return DependencyMap 90 | */ 91 | public function reduceEachDependency(\Closure $mappers) : DependencyMap 92 | { 93 | return $this->reduce(new self(), function (self $map, Dependency $from, Dependency $to) use ($mappers) { 94 | return $map->add($mappers($from), $mappers($to)); 95 | }); 96 | } 97 | 98 | public function toString() : string 99 | { 100 | return trim($this->reduce('', function (string $carry, Dependency $key, Dependency $value) { 101 | return $carry.$key->toString().' --> '.$value->toString().PHP_EOL; 102 | })); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Dependencies/DependencySet.php: -------------------------------------------------------------------------------- 1 | contains($dependency) 20 | || $dependency->count() === 0) { 21 | return $clone; 22 | } 23 | 24 | $clone->collection[] = $dependency; 25 | return $clone; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Dependencies/Interfaze.php: -------------------------------------------------------------------------------- 1 | ensureNamespaceIsValid($parts); 20 | $this->parts = $parts; 21 | } 22 | 23 | /** 24 | * @param array $parts 25 | * 26 | * @throws \InvalidArgumentException 27 | */ 28 | private function ensureNamespaceIsValid(array $parts) 29 | { 30 | if ($this->arrayContainsNotOnlyStrings($parts)) { 31 | throw new \InvalidArgumentException('Invalid namespace'); 32 | } 33 | } 34 | 35 | public function count() : int 36 | { 37 | return count($this->parts); 38 | } 39 | 40 | public function namespaze() : Namespaze 41 | { 42 | return $this; 43 | } 44 | 45 | /** 46 | * @return string[] 47 | */ 48 | public function parts() : array 49 | { 50 | return $this->parts; 51 | } 52 | 53 | public function reduceToDepth(int $maxDepth) : Dependency 54 | { 55 | if ($maxDepth === 0 || $this->count() === $maxDepth) { 56 | return $this; 57 | } 58 | 59 | return $this->count() < $maxDepth 60 | ? new NullDependency() 61 | : new self(array_slice($this->parts, 0, $maxDepth)); 62 | } 63 | 64 | public function reduceDepthFromLeftBy(int $reduction) : Dependency 65 | { 66 | return $reduction >= $this->count() 67 | ? new self([]) 68 | : new self(array_slice($this->parts, $reduction)); 69 | } 70 | 71 | public function equals(Dependency $other) : bool 72 | { 73 | return $this->toString() === $other->toString(); 74 | } 75 | 76 | public function toString() : string 77 | { 78 | return implode('\\', $this->parts); 79 | } 80 | 81 | public function __toString() : string 82 | { 83 | return $this->toString(); 84 | } 85 | 86 | public function inNamespaze(Namespaze $other) : bool 87 | { 88 | return $other->toString() !== '' 89 | && $this->toString() !== '' 90 | && strpos($this->toString(), $other->toString()) === 0; 91 | } 92 | 93 | /** 94 | * @param array $parts 95 | * 96 | * @return bool 97 | */ 98 | private function arrayContainsNotOnlyStrings(array $parts):bool 99 | { 100 | return Util::array_once($parts, function ($value) { 101 | return !is_string($value); 102 | }); 103 | } 104 | 105 | public function isNamespaced(): bool 106 | { 107 | return count($this->parts) > 0; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Dependencies/NullDependency.php: -------------------------------------------------------------------------------- 1 | toString(); 35 | } 36 | 37 | public function namespaze() : Namespaze 38 | { 39 | return new Namespaze([]); 40 | } 41 | 42 | public function inNamespaze(Namespaze $other) : bool 43 | { 44 | return false; 45 | } 46 | 47 | public function count() 48 | { 49 | return 0; 50 | } 51 | 52 | public function isNamespaced(): bool 53 | { 54 | return false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Dependencies/Trait_.php: -------------------------------------------------------------------------------- 1 | getPathname() . ' does not exist.'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/FileIsNotReadableException.php: -------------------------------------------------------------------------------- 1 | getPathname() . ' is not readable.'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/ParserException.php: -------------------------------------------------------------------------------- 1 | message = $message; 10 | $this->file = $file; 11 | } 12 | } -------------------------------------------------------------------------------- /src/Exceptions/PlantUmlNotInstalledException.php: -------------------------------------------------------------------------------- 1 | createEmptyDsm($dependencies->mapAllDependencies($mappers)); 16 | return $dependencies->reduce($emptyDsm, function (array $dsm, Dependency $from, Dependency $to) use ($mappers) : array { 17 | $from = $mappers($from)->toString(); 18 | $to = $mappers($to)->toString(); 19 | $dsm[$to][$from] += 1; 20 | return $dsm; 21 | }); 22 | } 23 | 24 | /** 25 | * @param $dependencies 26 | * 27 | * @return array 28 | */ 29 | private function createEmptyDsm(DependencySet $dependencies) 30 | { 31 | return $dependencies->reduce([], function (array $combined, Dependency $dependency) use ($dependencies) { 32 | $combined[$dependency->toString()] = array_combine( 33 | array_values($dependencies->toArray()), // keys: dependency name 34 | array_pad([], $dependencies->count(), 0) // values: [0, 0, 0, ... , 0] 35 | ); 36 | return $combined; 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Formatters/DependencyStructureMatrixHtmlFormatter.php: -------------------------------------------------------------------------------- 1 | dependencyStructureMatrixBuilder = $dependencyStructureMatrixBuilder; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function format(DependencyMap $all, \Closure $mappers = null) : string 27 | { 28 | return $this->buildHtmlTable( 29 | $this->dependencyStructureMatrixBuilder->buildMatrix($all, $mappers ?? Functional::id()) 30 | ); 31 | } 32 | 33 | /** 34 | * @param array $dependencyArray 35 | * 36 | * @return string 37 | */ 38 | private function buildHtmlTable(array $dependencyArray) : string 39 | { 40 | return '' 41 | .$this->tableHead($dependencyArray) 42 | .$this->tableBody($dependencyArray) 43 | ."
52 | "; 111 | } 112 | 113 | /** 114 | * @param array $dependencyArray 115 | * 116 | * @return string 117 | */ 118 | private function tableBody(array $dependencyArray) 119 | { 120 | $output = ''; 121 | $numIndex = 1; 122 | foreach ($dependencyArray as $dependencyRow => $dependencies) { 123 | $output .= "$numIndex: $dependencyRow"; 124 | foreach ($dependencies as $dependencyHeader => $count) { 125 | if ($dependencyRow === $dependencyHeader) { 126 | $output .= 'X'; 127 | } else { 128 | $output .= '' . $count . ''; 129 | } 130 | } 131 | $output .= ''; 132 | $numIndex += 1; 133 | } 134 | $output .= ''; 135 | return $output; 136 | } 137 | 138 | /** 139 | * @param array $dependencyArray 140 | * 141 | * @return string 142 | */ 143 | private function tableHead(array $dependencyArray) 144 | { 145 | $output = 'X'; 146 | for ($i = 1, $len = count($dependencyArray); $i <= $len; $i += 1) { 147 | $output .= "$i"; 148 | } 149 | return $output . ''; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Formatters/DotFormatter.php: -------------------------------------------------------------------------------- 1 | reduceEachDependency($mappers ?? Functional::id())->reduce('', function (string $carry, Dependency $from, Dependency $to) { 17 | return $carry."\t\"".str_replace('\\', '.', $from->toString().'" -> "'.$to->toString().'"').PHP_EOL; 18 | }) 19 | .'}'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Formatters/Formatter.php: -------------------------------------------------------------------------------- 1 | plantUmlNamespaceDefinitions($map).PHP_EOL 22 | .$this->dependenciesInPlantUmlFormat($map).PHP_EOL 23 | .'@enduml'; 24 | } 25 | 26 | private function dependenciesInPlantUmlFormat(DependencyMap $map) : string 27 | { 28 | return str_replace( 29 | ['-->', '\\'], 30 | ['--|>', '.'], 31 | $map->toString() 32 | ); 33 | } 34 | 35 | private function plantUmlNamespaceDefinitions(DependencyMap $map) : string 36 | { 37 | $namespaces = $map->reduce(new DependencySet(), function (DependencySet $set, Dependency $from, Dependency $to) { 38 | return $set 39 | ->add($from->namespaze()) 40 | ->add($to->namespaze()); 41 | }); 42 | return $this->printNamespaceTree( 43 | $this->buildNamespaceTree($namespaces) 44 | ); 45 | } 46 | 47 | private function buildNamespaceTree(DependencySet $namespaces) : array 48 | { 49 | return $namespaces->reduce([], function (array $total, Namespaze $namespaze) { 50 | $currentLevel = &$total; 51 | foreach ($namespaze->parts() as $part) { 52 | if (!array_key_exists($part, $currentLevel)) { 53 | $currentLevel[$part] = []; 54 | } 55 | $currentLevel = &$currentLevel[$part]; 56 | } 57 | return $total; 58 | }); 59 | } 60 | 61 | private function printNamespaceTree(array $buildNamespaceTree) : string 62 | { 63 | return Util::reduce($buildNamespaceTree, function (string $total, string $namespace, array $children) : string { 64 | return $total.'namespace '.$namespace.' {'.PHP_EOL.$this->printNamespaceTree($children).'}'.PHP_EOL; 65 | }, ''); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/OS/DotWrapper.php: -------------------------------------------------------------------------------- 1 | dotFormatter = $dotFormatter; 26 | $this->shellWrapper = $shellWrapper; 27 | } 28 | 29 | public function generate(DependencyMap $dependencies, \SplFileInfo $destination, bool $keepDotFile = false) 30 | { 31 | $this->ensureDotIsInstalled(); 32 | 33 | $dotFile = new \SplFileInfo($destination->getPath() 34 | .'/'.$destination->getBasename('.'.$destination->getExtension())); 35 | file_put_contents($dotFile->getPathname(), $this->dotFormatter->format($dependencies)); 36 | 37 | if ('dot' !== $destination->getExtension()) { 38 | $this->shellWrapper->run('dot -O -T'.$destination->getExtension().' '.$dotFile->getPathname()); 39 | if ($keepDotFile === false) { 40 | unlink($dotFile->getPathname()); 41 | } 42 | } 43 | } 44 | 45 | private function ensureDotIsInstalled() 46 | { 47 | if ($this->shellWrapper->run('dot -V') !== 0) { 48 | throw new DotNotInstalledException(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/OS/PhpFile.php: -------------------------------------------------------------------------------- 1 | ensureFileExists($file); 18 | $this->ensureFileIsReadable($file); 19 | $this->file = $file; 20 | } 21 | 22 | public function file() : \SplFileInfo 23 | { 24 | return $this->file; 25 | } 26 | 27 | public function equals(PhpFile $other) 28 | { 29 | return $this->file()->getPathname() === $other->file()->getPathname(); 30 | } 31 | 32 | public function code() 33 | { 34 | return @file_get_contents($this->file->getPathname()); 35 | } 36 | 37 | public function toString() : string 38 | { 39 | return (string) $this->file; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function __toString() : string 46 | { 47 | return $this->toString(); 48 | } 49 | 50 | private function ensureFileExists(\SplFileInfo $file) 51 | { 52 | if (!$file->isFile() && !$file->isDir()) { 53 | throw new FileDoesNotExistException($file); 54 | } 55 | } 56 | 57 | private function ensureFileIsReadable(\SplFileInfo $file) 58 | { 59 | if (!$file->isReadable()) { 60 | throw new FileIsNotReadableException($file); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/OS/PhpFileFinder.php: -------------------------------------------------------------------------------- 1 | isDir() 12 | ? $this->findInDir($file) 13 | : (new PhpFileSet())->add(new PhpFile($file)); 14 | } 15 | 16 | private function findInDir(\SplFileInfo $dir) : PhpFileSet 17 | { 18 | $collection = new PhpFileSet(); 19 | $regexIterator = new \RegexIterator( 20 | new \RecursiveIteratorIterator( 21 | new \RecursiveDirectoryIterator($dir->getPathname()) 22 | ), 23 | '/^.+\.php$/i', 24 | \RecursiveRegexIterator::GET_MATCH 25 | ); 26 | foreach ($regexIterator as $fileName) { 27 | $collection = $collection->add(new PhpFile(new \SplFileInfo($fileName[0]))); 28 | } 29 | 30 | return $collection; 31 | } 32 | 33 | /** 34 | * @param array $sources 35 | * 36 | * @return PhpFileSet 37 | */ 38 | public function getAllPhpFilesFromSources(array $sources) : PhpFileSet 39 | { 40 | return array_reduce( 41 | $sources, 42 | function (PhpFileSet $set, string $source) { 43 | return $set->addAll($this->find(new \SplFileInfo($source))); 44 | }, 45 | new PhpFileSet() 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/OS/PhpFileSet.php: -------------------------------------------------------------------------------- 1 | contains($file)) { 15 | return $clone; 16 | } 17 | 18 | $clone->collection[] = $file; 19 | return $clone; 20 | } 21 | 22 | public function addAll(PhpFileSet $otherCollection) : PhpFileSet 23 | { 24 | return $otherCollection->reduce(clone $this, function (self $set, PhpFile $file) { 25 | return $set->add($file); 26 | }); 27 | } 28 | 29 | public function contains($other) : bool 30 | { 31 | return $this->any(function (PhpFile $file) use ($other) { 32 | return $file->equals($other); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/OS/PlantUmlWrapper.php: -------------------------------------------------------------------------------- 1 | shell = $shell; 26 | $this->plantUmlFormatter = $plantUmlFormatter; 27 | } 28 | 29 | /** 30 | * @param DependencyMap $map 31 | * @param \SplFileInfo $destination 32 | * @param bool $keepUml 33 | * 34 | * @throws PlantUmlNotInstalledException 35 | */ 36 | public function generate(DependencyMap $map, \SplFileInfo $destination, bool $keepUml = false) 37 | { 38 | $this->ensurePlantUmlIsInstalled($this->shell); 39 | 40 | $umlDestination = preg_replace('/\.\w+$/', '.uml', $destination->getPathname()); 41 | file_put_contents($umlDestination, $this->plantUmlFormatter->format($map)); 42 | $this->shell->run('plantuml ' . $umlDestination); 43 | 44 | if ($keepUml === false) { 45 | unlink($umlDestination); 46 | } 47 | } 48 | 49 | /** 50 | * @param ShellWrapper $shell 51 | * 52 | * @throws PlantUmlNotInstalledException 53 | */ 54 | private function ensurePlantUmlIsInstalled(ShellWrapper $shell) 55 | { 56 | if ($shell->run('plantuml -version') !== 0) { 57 | throw new PlantUmlNotInstalledException(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/OS/ShellWrapper.php: -------------------------------------------------------------------------------- 1 | &1 /dev/null'; 10 | 11 | private $STD_ERR_PIPE_WIN = ' 2> NUL'; 12 | 13 | /** 14 | * @param string $command 15 | * 16 | * @return int return var 17 | */ 18 | public function run(string $command) : int 19 | { 20 | $output = []; 21 | $returnVar = 1; 22 | 23 | $command .= $this->isWindows() ? $this->STD_ERR_PIPE_WIN : $this->STD_ERR_PIPE; 24 | 25 | exec($command, $output, $returnVar); 26 | 27 | return $returnVar; 28 | } 29 | 30 | private function isWindows(): bool 31 | { 32 | return 0 === stripos(PHP_OS, 'WIN'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Util/AbstractCollection.php: -------------------------------------------------------------------------------- 1 | collection as $item) { 18 | if ($closure($item) === true) { 19 | return true; 20 | } 21 | } 22 | 23 | return false; 24 | } 25 | 26 | /** 27 | * @param \Closure $closure 28 | * 29 | * @return bool 30 | */ 31 | public function none(\Closure $closure) : bool 32 | { 33 | return !$this->any($closure); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function each(\Closure $closure) 40 | { 41 | foreach ($this->collection as $item) { 42 | $closure($item); 43 | } 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function mapToArray(\Closure $closure) : array 50 | { 51 | return array_map($closure, $this->collection); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function reduce($initial, \Closure $closure) 58 | { 59 | return array_reduce($this->collection, $closure, $initial); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function filter(\Closure $closure) : Collection 66 | { 67 | $clone = clone $this; 68 | $clone->collection = array_values(array_filter($this->collection, $closure)); 69 | 70 | return $clone; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function toArray() : array 77 | { 78 | return $this->collection; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function count() 85 | { 86 | return count($this->collection); 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function contains($other) : bool 93 | { 94 | return in_array($other, $this->collection); 95 | } 96 | 97 | public function toString() : string 98 | { 99 | return implode(PHP_EOL, $this->mapToArray(function ($x) { 100 | return $x->toString(); 101 | })); 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function __toString() : string 108 | { 109 | return $this->toString(); 110 | } 111 | 112 | public function equals(Collection $other) : bool 113 | { 114 | return $this->toString() === $other->toString(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Util/AbstractMap.php: -------------------------------------------------------------------------------- 1 | map as $item) { 21 | foreach ($item[self::$VALUE]->toArray() as $subItem) { 22 | if ($closure($item[self::$KEY], $subItem) === true) { 23 | return true; 24 | } 25 | } 26 | } 27 | return false; 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | public function none(\Closure $closure) : bool 34 | { 35 | return !$this->any($closure); 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function each(\Closure $closure) 42 | { 43 | foreach ($this->map as $item) { 44 | foreach ($item[self::$VALUE]->toArray() as $subItem) { 45 | $closure($item[self::$KEY], $subItem); 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * @inheritDoc 52 | */ 53 | public function mapToArray(\Closure $closure) : array 54 | { 55 | $xs = []; 56 | foreach ($this->map as $item) { 57 | foreach ($item[self::$VALUE]->toArray() as $subItem) { 58 | $xs[] = $closure($item[self::$KEY], $subItem); 59 | } 60 | } 61 | return $xs; 62 | } 63 | 64 | /** 65 | * @inheritDoc 66 | */ 67 | public function reduce($initial, \Closure $closure) 68 | { 69 | foreach ($this->map as $key => $item) { 70 | foreach ($item[self::$VALUE]->toArray() as $subItem) { 71 | $initial = $closure($initial, $item[self::$KEY], $subItem); 72 | } 73 | } 74 | return $initial; 75 | } 76 | 77 | /** 78 | * @inheritDoc 79 | */ 80 | public function filter(\Closure $closure) : Collection 81 | { 82 | $clone = clone $this; 83 | $clone->map = []; 84 | foreach ($this->map as $key => $item) { 85 | foreach ($item[self::$VALUE]->toArray() as $subItem) { 86 | if ($closure($item[self::$KEY], $subItem) === true) { 87 | $clone = $clone->add($item[self::$KEY], $subItem); 88 | } 89 | } 90 | } 91 | return $clone; 92 | } 93 | 94 | /** 95 | * @inheritDoc 96 | */ 97 | public function toArray() : array 98 | { 99 | return $this->map; 100 | } 101 | 102 | /** 103 | * @inheritDoc 104 | */ 105 | public function contains($other) : bool 106 | { 107 | foreach ($this->map as $key => $item) { 108 | if ($item[self::$KEY] instanceof $other && $item[self::$KEY]->equals($other)) { 109 | return true; 110 | } 111 | } 112 | return false; 113 | } 114 | 115 | /** 116 | * @inheritDoc 117 | */ 118 | abstract public function toString() : string; 119 | 120 | /** 121 | * @inheritDoc 122 | */ 123 | public function equals(Collection $other) : bool 124 | { 125 | return $this instanceof $other 126 | && $this->toString() === $other->toString(); 127 | } 128 | 129 | /** 130 | * @inheritDoc 131 | */ 132 | public function count() 133 | { 134 | return count($this->map); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Util/Collection.php: -------------------------------------------------------------------------------- 1 | internals = $internals; 42 | } 43 | 44 | public function dependencyFilter(): DependencyFilter 45 | { 46 | return new DependencyFilter($this->internals); 47 | } 48 | 49 | public function phpFileFinder(): PhpFileFinder 50 | { 51 | return new PhpFileFinder(); 52 | } 53 | 54 | public function parser(): Parser 55 | { 56 | return new DefaultParser((new ParserFactory())->create(ParserFactory::PREFER_PHP7)); 57 | } 58 | 59 | public function dependencyFactory(): DependencyFactory 60 | { 61 | return new DependencyFactory(); 62 | } 63 | 64 | public function staticAnalyser(): StaticAnalyser 65 | { 66 | return new StaticAnalyser( 67 | new NodeTraverser(), 68 | new DependencyInspectionVisitor( 69 | $this->dependencyFactory() 70 | ), 71 | $this->parser() 72 | ); 73 | } 74 | 75 | public function xDebugFunctionTraceAnalyser(): XDebugFunctionTraceAnalyser 76 | { 77 | return new XDebugFunctionTraceAnalyser($this->dependencyFactory()); 78 | } 79 | 80 | public function umlCommand(): UmlCommand 81 | { 82 | return new UmlCommand(new PlantUmlWrapper(new PlantUmlFormatter(), new ShellWrapper())); 83 | } 84 | 85 | public function dotCommand(): DotCommand 86 | { 87 | return new DotCommand(new DotWrapper(new DotFormatter(), new ShellWrapper())); 88 | } 89 | 90 | public function dsmCommand(): DsmCommand 91 | { 92 | return new DsmCommand(new DependencyStructureMatrixHtmlFormatter(new DependencyStructureMatrixBuilder())); 93 | } 94 | 95 | public function textCommand(): TextCommand 96 | { 97 | return new TextCommand(); 98 | } 99 | 100 | public function metricsCommand(): MetricsCommand 101 | { 102 | return new MetricsCommand(new Metrics()); 103 | } 104 | 105 | public function testFeaturesCommand(): TestFeaturesCommand 106 | { 107 | return new TestFeaturesCommand(); 108 | } 109 | 110 | public function dispatcher(): EventDispatcherInterface 111 | { 112 | return new Dispatcher( 113 | $this->staticAnalyser(), 114 | $this->xDebugFunctionTraceAnalyser(), 115 | $this->phpFileFinder(), 116 | $this->dependencyFilter() 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Util/Functional.php: -------------------------------------------------------------------------------- 1 | $value) { 21 | if ($fn($value, $index)) { 22 | return true; 23 | } 24 | } 25 | 26 | return false; 27 | } 28 | 29 | /** 30 | * PHP's array_reduce does not provide access to the key. This function 31 | * does the same as array produce, while providing access to the key. 32 | * 33 | * @param array $xs 34 | * @param \Closure $fn (mixed $carry, int|string $key, mixed $value) 35 | * @param $initial 36 | * 37 | * @return mixed 38 | */ 39 | public static function reduce(array $xs, \Closure $fn, $initial) 40 | { 41 | foreach ($xs as $key => $value) { 42 | $initial = $fn($initial, $key, $value); 43 | } 44 | return $initial; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/feature/DsmTest.php: -------------------------------------------------------------------------------- 1 | ([1-9]\d*).+.+@s', 15 | shell_exec(DEPHPEND_BIN.' dsm '.SRC_PATH.' --no-classes --depth=2 --format=html') 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/feature/HelpTest.php: -------------------------------------------------------------------------------- 1 | Mihaeu\PhpDependencies\Dependencies'.PHP_EOL 16 | .'Mihaeu\PhpDependencies\Analyser --> Mihaeu\PhpDependencies\OS'.PHP_EOL, 17 | shell_exec(DEPHPEND_BIN.' text '.SRC_PATH 18 | .' --no-classes --filter-from=Mihaeu\\\\PhpDependencies\\\\Analyser --exclude-regex="/Parser/"') 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/feature/UmlTest.php: -------------------------------------------------------------------------------- 1 | /dev/null 2>&1', $returnStatus); 14 | if ($returnStatus !== 0) { 15 | $this->markTestSkipped('No PlantUML installation found'); 16 | return; 17 | } 18 | 19 | $expected = << Mihaeu.PhpDependencies.Dependencies 37 | Mihaeu.PhpDependencies.OS --|> Mihaeu.PhpDependencies.Exceptions 38 | Mihaeu.PhpDependencies.OS --|> Mihaeu.PhpDependencies.Formatters 39 | Mihaeu.PhpDependencies.OS --|> Mihaeu.PhpDependencies.Util 40 | @enduml 41 | EOT; 42 | 43 | $tempFilePng = sys_get_temp_dir().'/dephpend-uml-test.png'; 44 | $tempFileUml = sys_get_temp_dir().'/dephpend-uml-test.uml'; 45 | shell_exec(DEPHPEND_BIN.' uml '.SRC_PATH.' --no-classes --keep-uml ' 46 | .'--output="'.$tempFilePng.'" -f Mihaeu\\\\PhpDependencies\\\\OS'); 47 | assertEquals( 48 | $expected, 49 | file_get_contents($tempFileUml) 50 | ); 51 | 52 | if (@unlink($tempFilePng) === false 53 | && @unlink($tempFileUml) === false) { 54 | $this->fail('Uml diagram or uml artifact was not created.'); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/feature/constants.php: -------------------------------------------------------------------------------- 1 | Singleton 2 | B 2 | # A --> C 3 | B 2 | B 2 | B 2 | # C --> D 3 | # C --> E 4 | B 2 | # A --> C 3 | # C --> B 4 | call(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/samples/MethodArgumentsAndReturnValueFromDocFeature.php: -------------------------------------------------------------------------------- 1 | # A --> B 2 | # A --> C 3 | # A --> D 4 | call(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/samples/ParamReturnThrowsInDocCommentFeature.php: -------------------------------------------------------------------------------- 1 | B 2 | B 2 | # A --> C 3 | # B --> C 4 | x(); 11 | } 12 | } 13 | 14 | class B 15 | { 16 | public function x() : C 17 | { 18 | return new C(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/samples/TypeHintsInMethodArgumentsFeature.php: -------------------------------------------------------------------------------- 1 | # A --> B 2 | # A --> C 3 | B 2 | # A --> C 3 | # A --> D 4 | createMock(Parser::class); 16 | $baseParser->method('parse')->willReturn(['test']); 17 | $parser = new DefaultParser($baseParser); 18 | assertEquals(['test'], $parser->parse('')); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/unit/Analyser/MetricsTest.php: -------------------------------------------------------------------------------- 1 | dependencies = (new DependencyMap()) 29 | ->add(new Clazz('A'), new Interfaze('B')) 30 | ->add(new Clazz('G'), new Interfaze('B')) 31 | ->add(new Clazz('R'), new Interfaze('B')) 32 | ->add(new Clazz('C'), new Trait_('F')) 33 | ->add(new AbstractClazz('D'), new Interfaze('E')) 34 | ->add(new Interfaze('B'), new Interfaze('E')) 35 | ->add(new Trait_('H'), new Interfaze('E')) 36 | ; 37 | $this->metrics = new Metrics(); 38 | } 39 | 40 | public function testAbstractnessWithNoDependency(): void 41 | { 42 | assertEquals(0, (new Metrics)->abstractness(new DependencyMap)); 43 | } 44 | 45 | public function testCountClasses(): void 46 | { 47 | assertEquals(4, $this->metrics->classCount($this->dependencies)); 48 | } 49 | 50 | public function testCountInterfaces(): void 51 | { 52 | assertEquals(1, $this->metrics->interfaceCount($this->dependencies)); 53 | } 54 | 55 | public function testCountAbstractClasses(): void 56 | { 57 | assertEquals(1, $this->metrics->abstractClassCount($this->dependencies)); 58 | } 59 | 60 | public function testCountTraits(): void 61 | { 62 | assertEquals(1, $this->metrics->traitCount($this->dependencies)); 63 | } 64 | 65 | public function testComputeAbstractness(): void 66 | { 67 | Assert::assertEqualsWithDelta(0.428, $this->metrics->abstractness($this->dependencies), 0.001); 68 | } 69 | 70 | public function testComputeAfferentCoupling(): void 71 | { 72 | assertEquals([ 73 | 'A' => 0, 74 | 'G' => 0, 75 | 'R' => 0, 76 | 'C' => 0, 77 | 'D' => 0, 78 | 'B' => 3, // three classes depend on B 79 | 'H' => 0, 80 | ], $this->metrics->afferentCoupling($this->dependencies)); 81 | } 82 | 83 | public function testComputeEfferentCoupling(): void 84 | { 85 | // all my classes depend only on one dependency 86 | assertEquals([ 87 | 'A' => 1, 88 | 'G' => 1, 89 | 'R' => 1, 90 | 'C' => 1, 91 | 'D' => 1, 92 | 'B' => 1, 93 | 'H' => 1, 94 | ], $this->metrics->efferentCoupling($this->dependencies)); 95 | } 96 | 97 | public function testComputeInstability(): void 98 | { 99 | assertEquals([ 100 | 'A' => 1, 101 | 'G' => 1, 102 | 'R' => 1, 103 | 'C' => 1, 104 | 'D' => 1, 105 | 'B' => 0.25, 106 | 'H' => 1, 107 | ], $this->metrics->instability($this->dependencies)); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/unit/Analyser/StaticAnalyserTest.php: -------------------------------------------------------------------------------- 1 | createMock(NodeTraverser::class); 34 | $this->dependencyInspectionVisitor = $this->createMock(DependencyInspectionVisitor::class); 35 | $this->parser = $this->createMock(Parser::class); 36 | 37 | $this->analyser = new StaticAnalyser( 38 | $nodeTraverser, 39 | $this->dependencyInspectionVisitor, 40 | $this->parser 41 | ); 42 | } 43 | 44 | public function testAnalyse(): void 45 | { 46 | $this->dependencyInspectionVisitor->method('dependencies')->willReturn(new DependencyMap()); 47 | $phpFile = $this->createMock(PhpFile::class); 48 | $phpFile->method('code')->willReturn(''); 49 | $dependencies = $this->analyser->analyse((new PhpFileSet())->add($phpFile)); 50 | assertEquals(new DependencyMap(), $dependencies); 51 | } 52 | 53 | public function testEnrichesExceptionWhenParserThrows(): void 54 | { 55 | $phpFile = $this->createMock(PhpFile::class); 56 | $phpFile->method('code')->willReturn(''); 57 | $phpFile->method('file')->willReturn(new \SplFileInfo('test.php')); 58 | $this->parser->method('parse') 59 | ->willThrowException(new Error('', [])); 60 | 61 | $this->expectException(ParserException::class); 62 | $this->analyser->analyse((new PhpFileSet())->add($phpFile)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/unit/Analyser/XDebugFunctionTraceAnalyserTest.php: -------------------------------------------------------------------------------- 1 | xDebugFunctionTraceAnalyser = new XDebugFunctionTraceAnalyser($this->createMock(DependencyFactory::class)); 25 | $this->tempFile = new \SplFileInfo(sys_get_temp_dir().'/'.'dephpend-trace.sample'); 26 | touch($this->tempFile->getPathname()); 27 | } 28 | 29 | protected function tearDown(): void 30 | { 31 | unlink($this->tempFile->getPathname()); 32 | } 33 | 34 | public function testAnalyse(): void 35 | { 36 | $this->writeContent([ 37 | [0, 1, 2, 3, 4, 'B->c', 6, 7, 8, 9, 10, 'class A'], 38 | [0, 1, 2, 3, 4, 'D->c', 6, 7, 8, 9, 10, 'class A'], 39 | ]); 40 | assertEquals( 41 | DependencyHelper::map(' 42 | B --> A 43 | D --> A 44 | '), 45 | $this->xDebugFunctionTraceAnalyser->analyse($this->tempFile) 46 | ); 47 | } 48 | 49 | public function testAnalyseIgnoresScalarValues(): void 50 | { 51 | $this->writeContent([ 52 | [0, 1, 2, 3, 4, 'B->c', 6, 7, 8, 9, 10, '???', 'class A'], 53 | [0, 1, 2, 3, 4, 'D->c', 6, 7, 8, 9, 10, 'string(10)'], 54 | [0, 1, 2, 3, 4, 'D->c', 6, 7, 8, 9, 10, 'long'], 55 | [0, 1, 2, 3, 4, 'D->c', 6, 7, 8, 9, 10, 'true'], 56 | [0, 1, 2, 3, 4, 'D->c', 6, 7, 8, 9, 10, 'false'], 57 | [0, 1, 2, 3, 4, 'D->c', 6, 7, 8, 9, 10, 'null'], 58 | [0, 1, 2, 3, 4, 'D->c', 6, 7, 8, 9, 10, 'int'], 59 | [0, 1, 2, 3, 4, 'D->c', 6, 7, 8, 9, 10, 'resource'], 60 | ]); 61 | assertEquals( 62 | DependencyHelper::map(' 63 | B --> A 64 | '), 65 | $this->xDebugFunctionTraceAnalyser->analyse($this->tempFile) 66 | ); 67 | } 68 | 69 | public function testThrowsExceptionIfFileCannotBeOpened(): void 70 | { 71 | $tmpFile = $this->createMock(SplFileInfo::class); 72 | $tmpFile->expects($this->once())->method('getPathname')->willReturn(''); 73 | $this->expectException(InvalidArgumentException::class); 74 | $this->xDebugFunctionTraceAnalyser->analyse($tmpFile); 75 | } 76 | 77 | private function createContent(array $data) : string 78 | { 79 | return array_reduce($data, function (string $carry, array $lineParts) { 80 | return $carry.implode("\t", $lineParts).PHP_EOL; 81 | }, ''); 82 | } 83 | 84 | private function writeContent(array $data): void 85 | { 86 | file_put_contents($this->tempFile->getPathname(), $this->createContent($data)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/unit/Cli/DispatcherTest.php: -------------------------------------------------------------------------------- 1 | staticAnalyser = $this->createMock(StaticAnalyser::class); 41 | $this->xDebugFunctionTraceAnalyser = $this->createMock(XDebugFunctionTraceAnalyser::class); 42 | $this->phpFileFinder = $this->createMock(PhpFileFinder::class); 43 | $this->dependencyFilter = $this->createMock(DependencyFilter::class); 44 | 45 | $this->dispatcher = new Dispatcher( 46 | $this->staticAnalyser, 47 | $this->xDebugFunctionTraceAnalyser, 48 | $this->phpFileFinder, 49 | $this->dependencyFilter 50 | ); 51 | } 52 | 53 | public function testTriggersOnlyOnNamedConsoleEvents(): void 54 | { 55 | $consoleEvent = $this->createMock(ConsoleEvent::class); 56 | $consoleEvent->expects(never())->method('getInput'); 57 | $this->dispatcher->dispatch($consoleEvent, 'other event'); 58 | } 59 | 60 | public function testTriggersOnlyOnConsoleEvents(): void 61 | { 62 | $consoleEvent = $this->createMock(GenericEvent::class); 63 | assertEquals( 64 | $consoleEvent, 65 | $this->dispatcher->dispatch(clone $consoleEvent, ConsoleEvents::COMMAND) 66 | ); 67 | } 68 | 69 | public function testInjectsDependenciesIntoConsoleEvents(): void 70 | { 71 | $command = $this->createMock(BaseCommand::class); 72 | 73 | $consoleEvent = $this->createMock(ConsoleEvent::class); 74 | $consoleEvent->method('getCommand')->willReturn($command); 75 | 76 | $input = $this->createMock(InputInterface::class); 77 | $input->method('getArgument')->with('source')->willReturn([]); 78 | $input->method('getOptions')->willReturn([]); 79 | $traceFile = sys_get_temp_dir(); 80 | $input->method('getOption')->with('dynamic')->willReturn($traceFile); 81 | $consoleEvent->method('getInput')->willReturn($input); 82 | 83 | $this->xDebugFunctionTraceAnalyser->expects(once())->method('analyse')->with($traceFile); 84 | $command->expects(once())->method('setDependencies'); 85 | $command->expects(once())->method('setPostProcessors'); 86 | $this->dispatcher->dispatch(clone $consoleEvent, ConsoleEvents::COMMAND); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/unit/Cli/DotCommandTest.php: -------------------------------------------------------------------------------- 1 | dotWrapper = $this->createMock(DotWrapper::class); 33 | $this->dotCommand = new DotCommand($this->dotWrapper); 34 | $this->input = $this->createMock(InputInterface::class); 35 | $this->output = $this->createMock(OutputInterface::class); 36 | } 37 | public function testGenerateDot(): void 38 | { 39 | $this->input->method('getArgument')->willReturn([sys_get_temp_dir()]); 40 | $this->input->method('getOptions')->willReturn([ 41 | 'output' => '/tmp/test.png', 42 | 'keep' => false, 43 | 'internals' => false, 44 | 'filter-namespace' => null, 45 | 'depth' => 0 46 | ]); 47 | $this->dotWrapper->expects(once())->method('generate'); 48 | 49 | $this->dotCommand->run( 50 | $this->input, 51 | $this->output 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/unit/Cli/DsmCommandTest.php: -------------------------------------------------------------------------------- 1 | dependencyStructureMatrixFormatter = $this->createMock(DependencyStructureMatrixHtmlFormatter::class); 40 | $this->dependencyFilter = $this->createMock(DependencyFilter::class); 41 | $this->dsmCommand = new DsmCommand($this->dependencyStructureMatrixFormatter); 42 | $this->input = $this->createMock(InputInterface::class); 43 | $this->output = $this->createMock(OutputInterface::class); 44 | } 45 | 46 | public function testPassesDependenciesToFormatter(): void 47 | { 48 | $this->input->method('getArgument')->willReturn([sys_get_temp_dir()]); 49 | $this->input->method('getOptions')->willReturn([ 50 | 'format' => 'html', 51 | 'internals' => false, 52 | 'filter-namespace' => null, 53 | 'depth' => 0, 54 | 'no-classes' => true 55 | ]); 56 | $this->dsmCommand = new DsmCommand($this->dependencyStructureMatrixFormatter); 57 | $dependencies = DependencyHelper::map('A --> B'); 58 | $this->dsmCommand->setDependencies($dependencies); 59 | 60 | $this->dependencyStructureMatrixFormatter->expects(once())->method('format')->with($dependencies); 61 | $this->dsmCommand->run($this->input, $this->output); 62 | } 63 | 64 | public function testDoesNotAllowOtherFormats(): void 65 | { 66 | $this->input->method('getArgument')->willReturn([sys_get_temp_dir()]); 67 | $this->input->method('getOptions')->willReturn(['format' => 'tiff']); 68 | $this->expectException(Exception::class); 69 | $this->expectExceptionMessage('Output format is not allowed (html)'); 70 | $this->dsmCommand->run($this->input, $this->output); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/unit/Cli/ErrorOutputTest.php: -------------------------------------------------------------------------------- 1 | symfonyStyle = $this->createMock(SymfonyStyle::class); 26 | $this->errorOutput = new ErrorOutput( 27 | $this->symfonyStyle 28 | ); 29 | } 30 | 31 | public function testWriteln(): void 32 | { 33 | $this->symfonyStyle->method('getErrorStyle')->willReturnSelf(); 34 | $this->symfonyStyle->expects(self::once())->method('writeln')->with('test'); 35 | $this->errorOutput->writeln('test'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/unit/Cli/MetricsCommandTest.php: -------------------------------------------------------------------------------- 1 | metrics = $this->createMock(Metrics::class); 34 | $this->metricsCommand = new MetricsCommand($this->metrics); 35 | $this->input = $this->createMock(InputInterface::class); 36 | $this->output = $this->createMock(OutputInterface::class); 37 | } 38 | 39 | public function testPrintsMetrics(): void 40 | { 41 | $this->input->method('getArgument')->willReturn([]); 42 | $this->input->method('getOption')->willReturn(true, 0); 43 | $this->input->method('getOptions')->willReturn(['internals' => false, 'filter-namespace' => null, 'depth' => 0]); 44 | 45 | $this->metrics->method('classCount')->willReturn(1); 46 | $this->metrics->method('abstractClassCount')->willReturn(2); 47 | $this->metrics->method('interfaceCount')->willReturn(3); 48 | $this->metrics->method('traitCount')->willReturn(4); 49 | $this->metrics->method('abstractness')->willReturn(5.0); 50 | $this->metrics->method('afferentCoupling')->willReturn(['A' => 1]); 51 | $this->metrics->method('efferentCoupling')->willReturn(['A' => 1]); 52 | $this->metrics->method('instability')->willReturn(['A' => 1]); 53 | 54 | $output = new BufferedOutput(); 55 | $this->metricsCommand->run($this->input, $output); 56 | assertEquals( 57 | '+--------------------+-------+ 58 | | Classes: | 1 | 59 | | Abstract classes: | 2 | 60 | | Interfaces: | 3 | 61 | | Traits: | 4 | 62 | | Abstractness: | 5.000 | 63 | +--------------------+-------+ 64 | +---+-------------------+-------------------+-------------+ 65 | | | Afferent Coupling | Efferent Coupling | Instability | 66 | +---+-------------------+-------------------+-------------+ 67 | | A | 1 | 1 | 1.00 | 68 | +---+-------------------+-------------------+-------------+ 69 | ', 70 | $output->fetch() 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/unit/Cli/TextCommandTest.php: -------------------------------------------------------------------------------- 1 | dependencyFilter = $this->createMock(DependencyFilter::class); 36 | $this->input = $this->createMock(InputInterface::class); 37 | $this->output = $this->createMock(OutputInterface::class); 38 | } 39 | 40 | public function testPrintsDependencies(): void 41 | { 42 | $dependencies = DependencyHelper::map(' 43 | A\\a\\1\\ClassA --> B\\a\\1\\ClassB 44 | A\\a\\1\\ClassA --> C\\a\\1\\ClassC 45 | B\\a\\1\\ClassB --> C\\a\\1\\ClassC 46 | '); 47 | $command = new TextCommand(); 48 | $command->setDependencies($dependencies); 49 | $command->setPostProcessors(Functional::id()); 50 | 51 | $this->input->method('getArgument')->willReturn([sys_get_temp_dir()]); 52 | $this->input->method('getOption')->willReturn(false, 0); 53 | $this->input->method('getOptions')->willReturn(['internals' => false, 'filter-namespace' => null, 'depth' => 0]); 54 | 55 | $this->output->expects(once()) 56 | ->method('writeln') 57 | ->with( 58 | 'A\\a\\1\\ClassA --> B\\a\\1\\ClassB'.PHP_EOL 59 | .'A\\a\\1\\ClassA --> C\\a\\1\\ClassC'.PHP_EOL 60 | .'B\\a\\1\\ClassB --> C\\a\\1\\ClassC' 61 | ); 62 | $command->run($this->input, $this->output); 63 | } 64 | 65 | public function testPrintsOnlyNamespacedDependencies(): void 66 | { 67 | $dependencies = DependencyHelper::map(' 68 | NamespaceA --> NamespaceB 69 | NamespaceA --> NamespaceC 70 | NamespaceB --> NamespaceC 71 | '); 72 | $command = new TextCommand(); 73 | $command->setDependencies($dependencies); 74 | 75 | $this->input->method('getArgument')->willReturn([sys_get_temp_dir()]); 76 | $this->input->method('getOptions')->willReturn(['internals' => false, 'filter-namespace' => null, 'depth' => 1]); 77 | 78 | $this->output->expects(once()) 79 | ->method('writeln') 80 | ->with( 81 | 'NamespaceA --> NamespaceB'.PHP_EOL 82 | .'NamespaceA --> NamespaceC'.PHP_EOL 83 | .'NamespaceB --> NamespaceC' 84 | ); 85 | $command->run($this->input, $this->output); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/unit/Cli/UmlCommandTest.php: -------------------------------------------------------------------------------- 1 | plantUmlWrapper = $this->createMock(PlantUmlWrapper::class); 36 | $this->umlCommand = new UmlCommand($this->plantUmlWrapper); 37 | $this->input = $this->createMock(InputInterface::class); 38 | $this->output = $this->createMock(OutputInterface::class); 39 | } 40 | 41 | public function testCheckIfSourceExists(): void 42 | { 43 | $this->input->method('getArgument')->willReturn(['/tsdfsfsfs']); 44 | $this->expectException(InvalidArgumentException::class); 45 | $this->expectExceptionMessage('File/Directory does not exist or is not readable.'); 46 | $this->umlCommand->run( 47 | $this->input, 48 | $this->output 49 | ); 50 | } 51 | 52 | public function testOutputHasToBeDefined(): void 53 | { 54 | $this->input->method('getArgument')->willReturn([sys_get_temp_dir()]); 55 | $this->input->method('getOptions')->willReturn(['output' => null]); 56 | $this->expectException(InvalidArgumentException::class); 57 | $this->expectExceptionMessage('Output not defined (use "help" for more information).'); 58 | $this->umlCommand->run( 59 | $this->input, 60 | $this->output 61 | ); 62 | } 63 | 64 | public function testChecksIfDestinationIsWritable(): void 65 | { 66 | $tempDirectory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . rand() . time(); 67 | mkdir($tempDirectory, 0444); 68 | 69 | $this->input->method('getArgument')->willReturn([$tempDirectory]); 70 | $this->input->method('getOptions')->willReturn(['output' => $tempDirectory . '/does_not_exist.png']); 71 | $this->expectException(InvalidArgumentException::class); 72 | $this->expectExceptionMessage('Destination is not writable.'); 73 | $this->umlCommand->run( 74 | $this->input, 75 | $this->output 76 | ); 77 | } 78 | 79 | public function testGenerateUml(): void 80 | { 81 | $this->input->method('getArgument')->willReturn([sys_get_temp_dir()]); 82 | $this->input->method('getOptions')->willReturn([ 83 | 'output' => '/tmp/test.png', 84 | 'keep-uml' => false, 85 | 'internals' => false, 86 | 'filter-namespace' => null, 87 | 'depth' => 0 88 | ]); 89 | $this->plantUmlWrapper->expects(once())->method('generate'); 90 | 91 | $this->umlCommand->run( 92 | $this->input, 93 | $this->output 94 | ); 95 | } 96 | 97 | public function testAcceptsOnlyAllowedFormats(): void 98 | { 99 | $this->input->method('getArgument')->willReturn([sys_get_temp_dir()]); 100 | $this->input->method('getOptions')->willReturn([ 101 | 'output' => sys_get_temp_dir().'/test.bmp' 102 | ]); 103 | 104 | $this->expectException(InvalidArgumentException::class); 105 | $this->expectExceptionMessage('Output format is not allowed (png)'); 106 | $this->umlCommand->run( 107 | $this->input, 108 | $this->output 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/unit/Dependencies/ClazzTest.php: -------------------------------------------------------------------------------- 1 | toString()); 22 | } 23 | 24 | public function testHasValue(): void 25 | { 26 | assertEquals('Name', new Clazz('Name')); 27 | assertEquals('Name', (new Clazz('Name'))->toString()); 28 | } 29 | 30 | public function testNamespace(): void 31 | { 32 | assertEquals(new Namespaze(['A', 'a']), (new Clazz('Name', new Namespaze(['A', 'a'])))->namespaze()); 33 | } 34 | 35 | public function testToStringWithNamespace(): void 36 | { 37 | assertEquals('A\\a\\ClassA', new Clazz('ClassA', new Namespaze(['A', 'a']))); 38 | } 39 | 40 | public function testEquals(): void 41 | { 42 | assertTrue((new Clazz('A'))->equals(new Clazz('A'))); 43 | } 44 | 45 | public function testEqualsIgnoresType(): void 46 | { 47 | assertTrue((new Clazz('A'))->equals(new Interfaze('A'))); 48 | } 49 | 50 | public function testDetectsIfClassHasNamespace(): void 51 | { 52 | assertTrue((new Clazz('Class', new Namespaze(['A'])))->hasNamespace()); 53 | } 54 | 55 | public function testDetectsIfClassHasNoNamespace(): void 56 | { 57 | assertFalse((new Clazz('Class'))->hasNamespace()); 58 | } 59 | 60 | public function testDepthWithoutNamespaceIsOne(): void 61 | { 62 | assertCount(1, new Clazz('A')); 63 | } 64 | 65 | public function testDepthWithNamespace(): void 66 | { 67 | assertCount(3, new Clazz('A', new Namespaze(['B', 'C']))); 68 | } 69 | 70 | public function testReduceWithDepthZero(): void 71 | { 72 | assertEquals( 73 | H::clazz('A\\B\\C\\D'), 74 | H::clazz('A\\B\\C\\D')->reduceToDepth(0) 75 | ); 76 | } 77 | 78 | public function testReduceToDepthTwoWithoutNamespacesProducesClass(): void 79 | { 80 | assertEquals(new Clazz('A'), (new Clazz('A'))->reduceToDepth(2)); 81 | } 82 | 83 | public function testReduceDepthToTwoProducesTopTwoNamespaces(): void 84 | { 85 | $clazz = H::clazz('A\\B\\C\\D'); 86 | assertEquals( 87 | new Namespaze(['A', 'B']), 88 | $clazz->reduceToDepth(2) 89 | ); 90 | } 91 | 92 | public function testReduceToDepthOfOneProducesOneNamespace(): void 93 | { 94 | assertEquals( 95 | new Namespaze(['A']), 96 | H::clazz('A\\B\\C\\D')->reduceToDepth(1) 97 | ); 98 | } 99 | 100 | public function testLeftReduceClassWithNamespace(): void 101 | { 102 | assertEquals( 103 | H::clazz('D'), 104 | H::clazz('A\\B\\C\\D')->reduceDepthFromLeftBy(3) 105 | ); 106 | } 107 | 108 | public function testCannotLeftReduceClassWithNamespaceByItsLength(): void 109 | { 110 | assertEquals( 111 | H::clazz('A\\B\\C\\D'), 112 | H::clazz('A\\B\\C\\D')->reduceDepthFromLeftBy(4) 113 | ); 114 | } 115 | 116 | /** 117 | * @see https://github.com/mihaeu/dephpend/issues/22 118 | */ 119 | public function testAcceptsNumbersAsFirstCharacterInName(): void 120 | { 121 | assertEquals('Vendor\\1Sub\\2Factor', new Clazz('2Factor', new Namespaze(['Vendor', '1Sub']))); 122 | } 123 | 124 | public function testCannotLeftReduceClassWithNamespaceByMoreThanItsLength(): void 125 | { 126 | assertEquals( 127 | H::clazz('A\\B\\C\\D'), 128 | H::clazz('A\\B\\C\\D')->reduceDepthFromLeftBy(5) 129 | ); 130 | } 131 | 132 | public function testDetectsIfInOtherNamespace(): void 133 | { 134 | assertTrue(DependencyHelper::clazz('A\\b\\T\\Test')->inNamespaze(DependencyHelper::namespaze('A\\b'))); 135 | assertTrue(DependencyHelper::clazz('A\\Test')->inNamespaze(DependencyHelper::namespaze('A'))); 136 | assertTrue(DependencyHelper::clazz(Collection::class)->inNamespaze(new Namespaze(['Mihaeu', 'PhpDependencies']))); 137 | } 138 | 139 | public function testDetectsIfNotInOtherNamespace(): void 140 | { 141 | assertFalse(DependencyHelper::clazz('Global')->inNamespaze(DependencyHelper::namespaze('A\\b\\T'))); 142 | } 143 | 144 | public function testThrowsExceptionIfNameNotValid(): void 145 | { 146 | $this->expectException(InvalidArgumentException::class); 147 | $this->expectExceptionMessage('Class name "Mihaeu\Test" is not valid.'); 148 | new Clazz('Mihaeu\\Test'); 149 | } 150 | 151 | public function testDetectsIfClassIsNotNamespaced(): void 152 | { 153 | assertFalse((new Clazz('NoNamespace'))->isNamespaced()); 154 | } 155 | 156 | public function testDetectsIfClassIsNamespaced(): void 157 | { 158 | assertTrue((new Clazz('HasNamespace', new Namespaze(['Vendor'])))->isNamespaced()); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/unit/Dependencies/DependencyFactoryTest.php: -------------------------------------------------------------------------------- 1 | clazzFactory = new DependencyFactory(); 20 | } 21 | 22 | public function testInvalidClassReturnsNullDependency(): void 23 | { 24 | assertInstanceOf( 25 | NullDependency::class, 26 | $this->clazzFactory->createClazzFromStringArray(['/']) 27 | ); 28 | } 29 | 30 | public function testCreatesClazzWithEmptyNamespace(): void 31 | { 32 | assertEquals(new Clazz('Test', new Namespaze([])), $this->clazzFactory->createClazzFromStringArray(['Test'])); 33 | } 34 | 35 | public function testCreateClazzWithNamespace(): void 36 | { 37 | assertEquals( 38 | new Clazz('Test', new Namespaze(['Mihaeu', 'PhpDependencies'])), 39 | $this->clazzFactory->createClazzFromStringArray(['Mihaeu', 'PhpDependencies', 'Test']) 40 | ); 41 | } 42 | 43 | public function testCreateInterfaze(): void 44 | { 45 | assertEquals(new AbstractClazz('Test', new Namespaze([])), $this->clazzFactory->createAbstractClazzFromStringArray(['Test'])); 46 | } 47 | 48 | public function testCreateAbstractClazz(): void 49 | { 50 | assertEquals(new Interfaze('Test', new Namespaze([])), $this->clazzFactory->createInterfazeFromStringArray(['Test'])); 51 | } 52 | 53 | public function testCreateTrait(): void 54 | { 55 | assertEquals(new Trait_('Test', new Namespaze([])), $this->clazzFactory->createTraitFromStringArray(['Test'])); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/unit/Dependencies/DependencyFilterTest.php: -------------------------------------------------------------------------------- 1 | filter = new DependencyFilter(['SplFileInfo']); 22 | } 23 | 24 | public function testRemovesInternals(): void 25 | { 26 | $dependencies = DependencyHelper::map('From --> To, SplFileInfo'); 27 | $expected = (new DependencyMap())->add(new Clazz('From'), new Clazz('To')); 28 | assertEquals($expected, $this->filter->removeInternals($dependencies)); 29 | } 30 | 31 | public function testFilterByDepthOne(): void 32 | { 33 | $dependencies = DependencyHelper::map(' 34 | From --> A\\a\\To 35 | B\\b\\FromOther --> SplFileInfo 36 | '); 37 | $expected = DependencyHelper::map(' 38 | From --> _A 39 | _B --> SplFileInfo 40 | '); 41 | $actual = $this->filter->filterByDepth($dependencies, 1); 42 | assertEquals($expected, $actual); 43 | } 44 | 45 | public function testMapUnderscoreNamespaces(): void 46 | { 47 | assertEquals( 48 | DependencyHelper::map(' 49 | A\\b\\c --> D\\e\\f 50 | F\\a --> D\\b 51 | '), 52 | $this->filter->mapNamespaces(DependencyHelper::map(' 53 | A_b_c --> D_e_f 54 | F_a --> D_b 55 | ')) 56 | ); 57 | } 58 | 59 | public function testMapUnderscoreNamespacesAlreadyNamespace(): void 60 | { 61 | assertEquals( 62 | DependencyHelper::map(' 63 | VendorA\\Tests\\DDC1209_1 --> a\\To 64 | A\\__b__\\c --> D\\e\\f 65 | '), 66 | $this->filter->mapNamespaces(DependencyHelper::map(' 67 | VendorA\\Tests\\DDC1209_1 --> a\\To 68 | A\\__b__\\c --> D\\e\\f 69 | ')) 70 | ); 71 | } 72 | 73 | public function testFilterByDepthThree(): void 74 | { 75 | $dependencies = DependencyHelper::map(' 76 | VendorA\\ProjectA\\PathA\\From --> VendorB\\ProjectB\\PathB\\To 77 | '); 78 | $expected = DependencyHelper::map('_VendorA\\ProjectA\\PathA --> _VendorB\\ProjectB\\PathB'); 79 | $actual = $this->filter->filterByDepth($dependencies, 3); 80 | assertEquals($expected, $actual); 81 | } 82 | 83 | public function testFilterByVendor(): void 84 | { 85 | $dependencies = DependencyHelper::map(' 86 | VendorA\\A --> VendorB\\A, VendorA\\C 87 | VendorB\\B --> VendorA\\A 88 | VendorC\\C --> VendorA\\A 89 | '); 90 | $expected = DependencyHelper::map(' 91 | VendorA\\A --> VendorA\\C 92 | '); 93 | assertEquals($expected, $this->filter->filterByNamespace($dependencies, 'VendorA')); 94 | } 95 | 96 | public function testFilterByDepth0ReturnsEqual(): void 97 | { 98 | $dependencies = DependencyHelper::map(' 99 | VendorA\\A --> VendorB\\A 100 | VendorA\\A --> VendorA\\C 101 | VendorB\\B --> VendorA\\A 102 | VendorC\\C --> VendorA\\A 103 | '); 104 | assertEquals($dependencies, $this->filter->filterByDepth($dependencies, 0)); 105 | } 106 | public function testRemoveClasses(): void 107 | { 108 | $expected = DependencyHelper::map(' 109 | _VendorA --> _VendorB 110 | _VendorB --> _VendorA 111 | '); 112 | $actual = $this->filter->filterClasses(DependencyHelper::map(' 113 | VendorA\\A --> VendorB\\A, VendorA\\C 114 | VendorB\\B --> VendorA\\A 115 | VendorC\\C --> B 116 | ')); 117 | assertEquals($expected, $actual); 118 | } 119 | 120 | public function testFilterFromDependencies(): void 121 | { 122 | assertEquals(DependencyHelper::map(' 123 | Good\\A --> Bad\\B 124 | Good\\B --> Good\\C 125 | '), $this->filter->filterByFromNamespace(DependencyHelper::map(' 126 | Good\\A --> Bad\\B 127 | Good\\B --> Good\\C 128 | Bad\\B --> Good\\A 129 | '), 'Good')); 130 | } 131 | 132 | public function testThrowsExceptionForBadRegex(): void 133 | { 134 | $options = [ 135 | 'internals' => false, 136 | 'filter-from' => 'A', 137 | 'depth' => 2, 138 | 'filter-namespace' => 'A', 139 | 'no-classes' => true, 140 | 'exclude-regex' => 'Missing brackets', 141 | 'underscore-namespaces' => true, 142 | ]; 143 | $this->expectException(InvalidArgumentException::class); 144 | $this->filter->filterByOptions(new DependencyMap(), $options); 145 | } 146 | 147 | public function testRunAllFilters(): void 148 | { 149 | $options = [ 150 | 'internals' => false, 151 | 'filter-from' => 'A', 152 | 'depth' => 2, 153 | 'filter-namespace' => 'A', 154 | 'no-classes' => true, 155 | 'exclude-regex' => '/Test/', 156 | 'underscore-namespaces' => true, 157 | ]; 158 | $dependencies = DependencyHelper::map(' 159 | A\\a\\z --> B\\b\\z 160 | A\\a\\z --> A\\b\\z 161 | A\\a\\Test --> A\\b\\z 162 | A_b_c --> A_b_z 163 | '); 164 | $actual = $this->filter->filterByOptions($dependencies, $options); 165 | $expected = DependencyHelper::map(' 166 | A\\a\\z --> A\\b\\z 167 | A\\b\\c --> A\\b\\z 168 | '); 169 | assertEquals($expected, $actual); 170 | } 171 | 172 | public function testExcludeByRegex(): void 173 | { 174 | assertEquals(DependencyHelper::map(' 175 | X --> Z 176 | '), $this->filter->excludeByRegex(DependencyHelper::map(' 177 | Test\\A --> B 178 | B --> Test\\C 179 | B --> C\\Test 180 | D --> Example 181 | Example --> Test\\Test 182 | X --> Z 183 | '), '/(Test)|(Example)/')); 184 | } 185 | 186 | public function testPostFilters(): void 187 | { 188 | $filters = $this->filter->postFiltersByOptions(['no-classes' => true, 'depth' => 1]); 189 | assertEquals(new Namespaze(['A']), $filters(new Clazz('Test', new Namespaze(['A', 'a'])))); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/unit/Dependencies/DependencySetTest.php: -------------------------------------------------------------------------------- 1 | add(new Clazz('Test')); 20 | $clazzCollection->each(function (Dependency $clazz) { 21 | assertEquals(new Clazz('Test'), $clazz); 22 | }); 23 | } 24 | 25 | public function testIsImmutable(): void 26 | { 27 | $clazzCollection = (new DependencySet()) 28 | ->add(new Clazz('Test')); 29 | $newCollectionAfterRefusingDuplicate = $clazzCollection->add(new Clazz('Test')); 30 | assertNotSame($clazzCollection, $newCollectionAfterRefusingDuplicate); 31 | } 32 | 33 | public function testDoesNotAcceptDuplicates(): void 34 | { 35 | $clazzCollection = (new DependencySet()) 36 | ->add(new Clazz('Test')); 37 | assertEquals($clazzCollection, $clazzCollection->add(new Clazz('Test'))); 38 | } 39 | 40 | public function testToArray(): void 41 | { 42 | $clazzCollection = (new DependencySet()) 43 | ->add(new Clazz('Test')); 44 | assertEquals([new Clazz('Test')], $clazzCollection->toArray()); 45 | } 46 | 47 | public function testToString(): void 48 | { 49 | $clazzCollection = (new DependencySet()) 50 | ->add(new Clazz('Test')) 51 | ->add(new Clazz('Test2')); 52 | assertEquals('Test'.PHP_EOL.'Test2', $clazzCollection->__toString()); 53 | } 54 | 55 | public function testFilter(): void 56 | { 57 | $expected = DependencyHelper::dependencySet('AB, AC'); 58 | assertEquals($expected, DependencyHelper::dependencySet('AB, AC, BA, CA')->filter(function (Dependency $dependency) { 59 | return strpos($dependency->toString(), 'A') === 0; 60 | })); 61 | } 62 | 63 | public function testReduce(): void 64 | { 65 | assertEquals('ABC', DependencyHelper::dependencySet('A, B, C')->reduce('', function (string $carry, Dependency $dependency) { 66 | return $carry.$dependency->toString(); 67 | })); 68 | } 69 | 70 | public function testNoneIsTrueWhenNoneMatches(): void 71 | { 72 | assertTrue(DependencyHelper::dependencySet('AB, AC, BA, CA')->none(function (Dependency $dependency) { 73 | return strpos($dependency->toString(), 'D') === 0; 74 | })); 75 | } 76 | 77 | public function testNoneIsFalseWhenSomeMatch(): void 78 | { 79 | assertFalse(DependencyHelper::dependencySet('AB, AC, BA, CA')->none(function (Dependency $dependency) { 80 | return strpos($dependency->toString(), 'A') === 0; 81 | })); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/unit/Dependencies/NamespazeTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 28 | new Namespaze([1]); 29 | } 30 | 31 | public function testDepthOfEmptyNamespaceIsZero(): void 32 | { 33 | assertCount(0, new Namespaze([])); 34 | } 35 | 36 | public function testDepthOfNamespace(): void 37 | { 38 | assertCount(2, new Namespaze(['A', 'B'])); 39 | } 40 | 41 | public function testReducingDepthLowerThanPossibleProducesNullDependency(): void 42 | { 43 | assertInstanceOf(NullDependency::class, (new Namespaze(['Test']))->reduceToDepth(3)); 44 | } 45 | 46 | public function testReduceToMaxDepth(): void 47 | { 48 | assertEquals(new Namespaze(['A', 'B']), (new Namespaze(['A', 'B', 'C', 'D']))->reduceToDepth(2)); 49 | } 50 | 51 | public function testDoNotReduceForMaxDepthZero(): void 52 | { 53 | assertEquals(new Namespaze(['A', 'B']), (new Namespaze(['A', 'B']))->reduceToDepth(0)); 54 | } 55 | 56 | public function testLeftReduceNamespace(): void 57 | { 58 | assertEquals(new Namespaze(['C']), (new Namespaze(['A', 'B', 'C']))->reduceDepthFromLeftBy(2)); 59 | } 60 | 61 | public function testReduceSameAsLengthProducesEmptyNamespace(): void 62 | { 63 | assertEquals(new Namespaze([]), (new Namespaze(['A', 'B', 'C']))->reduceDepthFromLeftBy(3)); 64 | } 65 | 66 | public function testReduceMoreThanLengthProducesEmptyNamespace(): void 67 | { 68 | assertEquals(new Namespaze([]), (new Namespaze(['A', 'B', 'C']))->reduceDepthFromLeftBy(5)); 69 | } 70 | 71 | public function testEquals(): void 72 | { 73 | assertTrue((new Namespaze(['A', 'B']))->equals(new Namespaze(['A', 'B']))); 74 | assertTrue((new Namespaze([]))->equals(new Namespaze([]))); 75 | assertFalse((new Namespaze(['A', 'B']))->equals(new Namespaze(['A']))); 76 | assertFalse((new Namespaze(['A', 'B']))->equals(new Namespaze([]))); 77 | } 78 | 79 | public function testPartsByIndex(): void 80 | { 81 | assertEquals(new Namespaze(['1']), (new Namespaze(['1', '2']))->parts()[0]); 82 | assertEquals(new Namespaze(['2']), (new Namespaze(['1', '2']))->parts()[1]); 83 | } 84 | 85 | public function testNamespazeReturnsItself(): void 86 | { 87 | assertEquals(new Namespaze(['1', '2']), (new Namespaze(['1', '2']))->namespaze()); 88 | } 89 | 90 | public function testDetectsIfInOtherNamespace(): void 91 | { 92 | assertTrue((new Namespaze(['A', 'b', 'T']))->inNamespaze(new Namespaze(['A', 'b', 'T']))); 93 | assertTrue((new Namespaze(['A', 'b', 'T']))->inNamespaze(new Namespaze(['A']))); 94 | } 95 | 96 | public function testDetectsIfNotInOtherNamespace(): void 97 | { 98 | assertFalse((new Namespaze(['XZY', 'b', 'T']))->inNamespaze(new Namespaze(['A', 'b', 'T']))); 99 | assertFalse((new Namespaze([]))->inNamespaze(new Namespaze(['A', 'b', 'T']))); 100 | assertFalse((new Namespaze(['XZY', 'b', 'T']))->inNamespaze(new Namespaze([]))); 101 | } 102 | 103 | public function testEmptyNamespaceIsNotNamespaced(): void 104 | { 105 | assertFalse((new Namespaze([]))->isNamespaced()); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/unit/Dependencies/NullDependencyTest.php: -------------------------------------------------------------------------------- 1 | reduceToDepth(99)); 17 | } 18 | 19 | public function testReduceDepthFromLeftBy(): void 20 | { 21 | assertEquals(new NullDependency(), (new NullDependency())->reduceDepthFromLeftBy(99)); 22 | } 23 | 24 | public function testEquals(): void 25 | { 26 | assertFalse((new NullDependency())->equals(new NullDependency())); 27 | } 28 | 29 | public function testToString(): void 30 | { 31 | assertEquals('', (new NullDependency())->__toString()); 32 | } 33 | 34 | public function testNamespaze(): void 35 | { 36 | assertEquals(new Namespaze([]), (new NullDependency())->namespaze()); 37 | } 38 | 39 | public function testInNamespazeIsFalseForEmptyNamespace(): void 40 | { 41 | assertFalse((new NullDependency())->inNamespaze(new Namespaze([]))); 42 | } 43 | 44 | public function testInNamespazeIsFalseForEveryNamespace(): void 45 | { 46 | assertFalse((new NullDependency())->inNamespaze(new Namespaze(['A']))); 47 | } 48 | 49 | public function testCountIsAlwaysZero(): void 50 | { 51 | assertCount(0, new NullDependency()); 52 | } 53 | 54 | public function testIsNotNamespaced(): void 55 | { 56 | assertFalse((new NullDependency())->isNamespaced()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/unit/DependencyHelper.php: -------------------------------------------------------------------------------- 1 | DepB, DepC 24 | * DepC --> DepD, DepE 25 | * 26 | * @return DependencyMap 27 | * 28 | * @throws InvalidArgumentException 29 | */ 30 | public static function map(string $input) : DependencyMap 31 | { 32 | $lines = preg_split('/\v+/', $input, -1, PREG_SPLIT_NO_EMPTY); 33 | $array_reduce = array_reduce( 34 | $lines, 35 | function (DependencyMap $map, string $line) { 36 | if (empty(trim($line))) { 37 | return $map; 38 | } 39 | $dependencyPair = self::dependencyPair($line); 40 | return $map->addSet($dependencyPair[0], $dependencyPair[1]); 41 | }, 42 | new DependencyMap() 43 | ); 44 | return $array_reduce; 45 | } 46 | 47 | /** 48 | * @param string $input format: NamespaceA\\ClassA 49 | * 50 | * @return Clazz 51 | */ 52 | public static function clazz(string $input): Dependency 53 | { 54 | return (new DependencyFactory())->createClazzFromStringArray(explode('\\', $input)); 55 | } 56 | 57 | /** 58 | * @param string $input format: NamespaceA\\a 59 | * 60 | * @return Namespaze 61 | */ 62 | public static function namespaze(string $input) : Namespaze 63 | { 64 | return new Namespaze(explode('\\', $input)); 65 | } 66 | 67 | /** 68 | * @param string $input format: NamespaceA\\ClassA --> NamespaceB\\ClassB, NamespaceC\\ClassC 69 | * 70 | * @return array 71 | */ 72 | public static function dependencyPair(string $input) : array 73 | { 74 | $tokens = explode('-->', str_replace(' ', '', $input)); 75 | return [self::dependency($tokens[0]), self::dependencySet($tokens[1])]; 76 | } 77 | 78 | /** 79 | * @param string $input format: NamespaceA\\ClassA, NamespaceB\\ClassB, NamespaceC\\ClassC 80 | * 81 | * @return DependencySet 82 | */ 83 | public static function dependencySet(string $input) : DependencySet 84 | { 85 | $set = new DependencySet(); 86 | if ($input === '_') { 87 | return $set; 88 | } 89 | 90 | foreach (explode(',', $input) as $token) { 91 | $set = $set->add(self::dependency($token)); 92 | } 93 | return $set; 94 | } 95 | 96 | private static function dependency(string $input) : Dependency 97 | { 98 | $input = str_replace(' ', '', $input); 99 | if (strpos($input, '_') === 0) { 100 | return self::namespaze(substr($input, 1)); 101 | } 102 | return self::clazz($input); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/unit/DependencyHelperTest.php: -------------------------------------------------------------------------------- 1 | add( 21 | new Clazz('DepA', new Namespaze(['A'])), 22 | new Clazz('DepB', new Namespaze(['B'])) 23 | )->add( 24 | new Clazz('DepC', new Namespaze(['C'])), 25 | new Clazz('DepD', new Namespaze(['D'])) 26 | ); 27 | assertEquals($expected, DependencyHelper::map(' 28 | A\\DepA --> B\\DepB 29 | C\\DepC --> D\\DepD 30 | ')); 31 | } 32 | 33 | public function testConvertMultipleDependencies(): void 34 | { 35 | $expected = (new DependencyMap())->add( 36 | new Clazz('DepA', new Namespaze(['A'])), 37 | new Clazz('DepB', new Namespaze(['B'])) 38 | )->add( 39 | new Clazz('DepA', new Namespaze(['A'])), 40 | new Clazz('DepD', new Namespaze(['D'])) 41 | ); 42 | assertEquals($expected, DependencyHelper::map(' 43 | A\\DepA --> B\\DepB, D\\DepD 44 | ')); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/unit/Formatters/DependencyStructureMatrixBuilderTest.php: -------------------------------------------------------------------------------- 1 | builder = new DependencyStructureMatrixBuilder(); 20 | } 21 | 22 | public function testBuildMatrixFromClassesWithoutNamespaces(): void 23 | { 24 | $dependencies = DependencyHelper::map(' 25 | A --> D, B 26 | B --> D 27 | C --> A 28 | D --> B 29 | '); 30 | assertEquals([ 31 | 'A' => ['A' => 0, 'B' => 0, 'C' => 1, 'D' => 0], 32 | 'B' => ['A' => 1, 'B' => 0, 'C' => 0, 'D' => 1], 33 | 'C' => ['A' => 0, 'B' => 0, 'C' => 0, 'D' => 0], 34 | 'D' => ['A' => 1, 'B' => 1, 'C' => 0, 'D' => 0], 35 | ], $this->builder->buildMatrix($dependencies, Functional::id())); 36 | } 37 | 38 | public function testBuildMatrixFromClassesWithNamespaces(): void 39 | { 40 | $dependencies = DependencyHelper::map(' 41 | AA\\A --> DD\\D, BB\\B 42 | BB\\B --> DD\\D 43 | CC\\C --> AA\\A 44 | DD\\D --> BB\\B 45 | '); 46 | assertEquals([ 47 | 'AA\\A' => ['AA\\A' => 0, 'BB\\B' => 0, 'CC\\C' => 1, 'DD\\D' => 0], 48 | 'BB\\B' => ['AA\\A' => 1, 'BB\\B' => 0, 'CC\\C' => 0, 'DD\\D' => 1], 49 | 'CC\\C' => ['AA\\A' => 0, 'BB\\B' => 0, 'CC\\C' => 0, 'DD\\D' => 0], 50 | 'DD\\D' => ['AA\\A' => 1, 'BB\\B' => 1, 'CC\\C' => 0, 'DD\\D' => 0], 51 | ], $this->builder->buildMatrix($dependencies, Functional::id())); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/unit/Formatters/DependencyStructureMatrixHtmlFormatterTest.php: -------------------------------------------------------------------------------- 1 | dependencyStructureMatrixBuilder = $this->createMock(DependencyStructureMatrixBuilder::class); 26 | $this->dependencyStructureMatrixHtmlFormatter = new DependencyStructureMatrixHtmlFormatter($this->dependencyStructureMatrixBuilder); 27 | } 28 | 29 | public function testFormatsHtml(): void 30 | { 31 | $this->dependencyStructureMatrixBuilder->method('buildMatrix')->willReturn([ 32 | 'A' => ['A' => 0, 'B' => 1, 'C' => 1], 33 | 'B' => ['A' => 0, 'B' => 0, 'C' => 1], 34 | 'C' => ['A' => 0, 'B' => 0, 'C' => 0] 35 | ]); 36 | Assert::assertStringContainsString( 37 | '' 38 | .'' 39 | .'' 40 | .'' 41 | .'' 42 | .'' 43 | .'
X123
1: AX11
2: B0X1
3: C00X
', 44 | $this->dependencyStructureMatrixHtmlFormatter->format(new DependencyMap(), function ($x) { 45 | return $x; 46 | }) 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/unit/Formatters/DotFormatterTest.php: -------------------------------------------------------------------------------- 1 | \"B\"".PHP_EOL 17 | ."\t\"C\" -> \"D\"".PHP_EOL 18 | ."\t\"A.b\" -> \"D.c\"".PHP_EOL 19 | .'}' 20 | ; 21 | 22 | assertEquals($expected, (new DotFormatter())->format(DependencyHelper::map(' 23 | A --> B 24 | C --> D 25 | A\\b --> D\\c 26 | '))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/unit/Formatters/PlantUmlFormatterTest.php: -------------------------------------------------------------------------------- 1 | plantUmlFormatter = new PlantUmlFormatter(); 21 | } 22 | 23 | public function testFormat(): void 24 | { 25 | $dependencyCollection = DependencyHelper::map('ClassA --> ClassB, ClassC'); 26 | assertEquals("@startuml\n\n" 27 | ."ClassA --|> ClassB\n" 28 | ."ClassA --|> ClassC\n" 29 | .'@enduml', $this->plantUmlFormatter->format($dependencyCollection)); 30 | } 31 | 32 | public function testFormatsNestedNamespaces(): void 33 | { 34 | assertEquals('@startuml 35 | namespace A { 36 | namespace b { 37 | } 38 | } 39 | namespace B { 40 | namespace a { 41 | } 42 | namespace b { 43 | } 44 | } 45 | 46 | A.b.C1 --|> A.b.C2 47 | B.a.C1 --|> B.b.C2 48 | @enduml', $this->plantUmlFormatter->format(DependencyHelper::map(' 49 | A\\b\\C1 --> A\\b\\C2 50 | B\\a\\C1 --> B\\b\\C2 51 | '))); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/unit/OS/DotWrapperTest.php: -------------------------------------------------------------------------------- 1 | shellWrapper = $this->createMock(ShellWrapper::class); 31 | $this->dotFormatter = $this->createMock(DotFormatter::class); 32 | $this->dotWrapper = new DotWrapper($this->dotFormatter, $this->shellWrapper); 33 | } 34 | 35 | public function testThrowsExceptionIfDotIsNotInstalled(): void 36 | { 37 | $this->shellWrapper->method('run')->willReturn(1); 38 | 39 | $this->expectException(DotNotInstalledException::class); 40 | $this->dotWrapper->generate(new DependencyMap(), new SplFileInfo(__FILE__)); 41 | } 42 | 43 | public function testRunsDot(): void 44 | { 45 | $root = vfsStream::setup()->url(); 46 | 47 | $this->shellWrapper 48 | ->expects(exactly(2)) 49 | ->method('run') 50 | ->withConsecutive( 51 | ['dot -V'], 52 | ['dot -O -Tpng '.$root.'/test'] 53 | ) 54 | ; 55 | $this->dotWrapper->generate(new DependencyMap(), new SplFileInfo($root.'/test.png'), true); 56 | } 57 | 58 | public function testKeepsDotFiles(): void 59 | { 60 | $root = vfsStream::setup()->url(); 61 | $testFile = new SplFileInfo($root.'/test'); 62 | assertFileNotExists($testFile->getPathname()); 63 | $this->dotWrapper->generate(new DependencyMap(), new SplFileInfo($testFile->getPathname()), true); 64 | assertFileExists($testFile->getPathname()); 65 | } 66 | 67 | 68 | public function testRemovesDotFiles(): void 69 | { 70 | $root = vfsStream::setup()->url(); 71 | $testFile = new SplFileInfo($root.'/test'); 72 | $this->dotWrapper->generate(new DependencyMap(), new SplFileInfo($root.'/test.png'), false); 73 | assertFileNotExists($testFile->getPathname()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/unit/OS/PhpFileFinderTest.php: -------------------------------------------------------------------------------- 1 | finder = new PhpFileFinder(); 22 | } 23 | 24 | public function testFindsSingleFileInFlatStructure(): void 25 | { 26 | $mockDir = vfsStream::setup('root', null, [ 27 | 'root' => [ 28 | 'someFile.php' => 'url()); 32 | $expected = (new PhpFileSet()) 33 | ->add(new PhpFile(new SplFileInfo($mockDir->url().'/root/someFile.php'))); 34 | assertEquals($expected, $this->finder->find($dir)); 35 | } 36 | 37 | public function testFindsFilesInDeepStructure(): void 38 | { 39 | $mockDir = vfsStream::setup('root', null, [ 40 | 'root' => [ 41 | 'someFile.php' => ' [ 43 | 'dirB' => [ 44 | 'dirC' => [ 45 | 'fileInC.php' => ' ' ' 'url()); 55 | $expected = (new PhpFileSet()) 56 | ->add(new PhpFile(new SplFileInfo($mockDir->url().'/root/someFile.php'))) 57 | ->add(new PhpFile(new SplFileInfo($mockDir->url().'/root/dirA/fileInA.php'))) 58 | ->add(new PhpFile(new SplFileInfo($mockDir->url().'/root/dirA/dirB/fileInB.php'))) 59 | ->add(new PhpFile(new SplFileInfo($mockDir->url().'/root/dirA/dirB/fileInB2.php'))) 60 | ->add(new PhpFile(new SplFileInfo($mockDir->url().'/root/dirA/dirB/dirC/fileInC.php'))); 61 | assertEquals($expected, $this->finder->find($dir)); 62 | } 63 | 64 | public function testFindsNothingIfThereIsNothing(): void 65 | { 66 | $mockDir = vfsStream::setup('root', null, [ 67 | 'root' => [ 68 | 'someFile.js' => 'console.log("Hello World!");', 69 | 'dirA' => [ 70 | 'dirB' => [ 71 | 'dirC' => [ 72 | 'fileInC.js' => '', 73 | ], 74 | 'fileInB.js' => '', 75 | 'fileInB2.js' => '', 76 | ], 77 | 'fileInA.js' => '', 78 | ], 79 | ], 80 | ]); 81 | $dir = new SplFileInfo($mockDir->url()); 82 | assertEmpty($this->finder->find($dir)); 83 | } 84 | 85 | public function testFindFilesInDeeplyNestedDirectory(): void 86 | { 87 | $mockDir = vfsStream::setup('root', null, [ 88 | 'root' => [ 89 | 'someFile.php' => 'console.log("Hello World!");', 90 | 'dirA' => [ 91 | 'fileInA.php' => '', 92 | ], 93 | 'dirB' => [ 94 | 'dirC' => [ 95 | 'fileInC.php' => '', 96 | ], 97 | 'fileInB.php' => '', 98 | 'fileInB2.php' => '', 99 | ], 100 | ], 101 | ]); 102 | $expected = (new PhpFileSet()) 103 | ->add(new PhpFile(new SplFileInfo($mockDir->url().'/root/dirA/fileInA.php'))) 104 | ->add(new PhpFile(new SplFileInfo($mockDir->url().'/root/dirB/fileInB.php'))) 105 | ->add(new PhpFile(new SplFileInfo($mockDir->url().'/root/dirB/fileInB2.php'))) 106 | ->add(new PhpFile(new SplFileInfo($mockDir->url().'/root/dirB/dirC/fileInC.php'))); 107 | $actual = $this->finder->getAllPhpFilesFromSources([ 108 | $mockDir->url().'/root/dirA', 109 | $mockDir->url().'/root/dirB', 110 | ]); 111 | assertEquals($expected, $actual); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/unit/OS/PhpFileSetTest.php: -------------------------------------------------------------------------------- 1 | add($file); 21 | $collection2 = (new PhpFileSet()) 22 | ->add($file); 23 | assertTrue($collection1->equals($collection2)); 24 | } 25 | 26 | public function testDoesNotAllowDuplicated(): void 27 | { 28 | $set = (new PhpFileSet()) 29 | ->add(new PhpFile(new SplFileInfo(__DIR__))) 30 | ->add(new PhpFile(new SplFileInfo(__DIR__))); 31 | assertCount(1, $set); 32 | } 33 | 34 | public function testIsImmutable(): void 35 | { 36 | $set = (new PhpFileSet()) 37 | ->add(new PhpFile(new SplFileInfo(__DIR__))); 38 | $setAfterRefusingDuplicate = $set->add(new PhpFile(new SplFileInfo(__DIR__))); 39 | assertNotSame($set, $setAfterRefusingDuplicate); 40 | } 41 | 42 | public function testNotEquals(): void 43 | { 44 | $collection1 = (new PhpFileSet()) 45 | ->add(new PhpFile(new SplFileInfo(sys_get_temp_dir()))); 46 | $collection2 = (new PhpFileSet()) 47 | ->add(new PhpFile(new SplFileInfo(__DIR__))); 48 | assertFalse($collection1->equals($collection2)); 49 | } 50 | 51 | public function testCount0WhenEmpty(): void 52 | { 53 | $collection1 = new PhpFileSet(); 54 | assertCount(0, $collection1); 55 | } 56 | 57 | public function testCount(): void 58 | { 59 | assertCount(2, (new PhpFileSet()) 60 | ->add(new PhpFile(new SplFileInfo(__DIR__))) 61 | ->add(new PhpFile(new SplFileInfo(__DIR__.'/../../../composer.json')))); 62 | } 63 | 64 | public function testEach(): void 65 | { 66 | $collection1 = (new PhpFileSet())->add(new PhpFile(new SplFileInfo(__DIR__))); 67 | $collection1->each(function (PhpFile $file) { 68 | assertEquals(new PhpFile(new SplFileInfo(__DIR__)), $file); 69 | }); 70 | } 71 | 72 | public function testMapToArray(): void 73 | { 74 | $collection1 = (new PhpFileSet()) 75 | ->add(new PhpFile(new SplFileInfo(__DIR__))) 76 | ->add(new PhpFile(new SplFileInfo(__FILE__))) 77 | ->toArray(); 78 | assertEquals(new PhpFile(new SplFileInfo(__DIR__)), $collection1[0]); 79 | assertEquals(new PhpFile(new SplFileInfo(__DIR__)), $collection1[1]); 80 | } 81 | 82 | public function testAddAll(): void 83 | { 84 | $collection1 = new PhpFileSet(); 85 | $collection2 = (new PhpFileSet()) 86 | ->add(new PhpFile(new SplFileInfo(__DIR__))) 87 | ->add(new PhpFile(new SplFileInfo(__FILE__))); 88 | $combinedCollection = $collection1->addAll($collection2)->toArray(); 89 | assertCount(2, $combinedCollection); 90 | assertEquals(new PhpFile(new SplFileInfo(__DIR__)), $combinedCollection[0]); 91 | assertEquals(new PhpFile(new SplFileInfo(__FILE__)), $combinedCollection[1]); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/unit/OS/PhpFileTest.php: -------------------------------------------------------------------------------- 1 | equals($file2)); 26 | } 27 | 28 | public function testNotEquals(): void 29 | { 30 | $file1 = new PhpFile(new SplFileInfo(sys_get_temp_dir())); 31 | $file2 = new PhpFile(new SplFileInfo(__DIR__)); 32 | assertFalse($file1->equals($file2)); 33 | } 34 | 35 | public function testReturnsCode(): void 36 | { 37 | $code = ' $code, 40 | ]); 41 | $file = new PhpFile(new SplFileInfo($mockDir->url().'/someFile.php')); 42 | assertEquals($code, $file->code()); 43 | } 44 | 45 | public function testToString(): void 46 | { 47 | Assert::assertStringContainsString('PhpFileTest.php', (new PhpFile(new SplFileInfo(__FILE__)))->__toString()); 48 | } 49 | 50 | public function testThrowsExceptionsIfFileDoesNotExist(): void 51 | { 52 | $this->expectException(FileDoesNotExistException::class); 53 | new PhpFile(new SplFileInfo('akdsjajdlsad')); 54 | } 55 | 56 | public function testThrowsExceptionIfFileIsNotReadable(): void 57 | { 58 | $tmpFiles = $this->createMock(SplFileInfo::class); 59 | $tmpFiles->expects($this->once())->method('isFile')->willReturn(true); 60 | $tmpFiles->expects($this->once())->method('isReadable')->willReturn(false); 61 | 62 | $this->expectException(FileIsNotReadableException::class); 63 | new PhpFile($tmpFiles); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/unit/OS/PlantUmlWrapperTest.php: -------------------------------------------------------------------------------- 1 | shellWrapper = $this->createMock(ShellWrapper::class); 29 | $this->plantUmlFormatter = $this->createMock(PlantUmlFormatter::class); 30 | } 31 | 32 | public function testDetectsIfPlantUmlIsNotInstalled(): void 33 | { 34 | $this->shellWrapper->method('run')->willReturn(1); 35 | $plantUml = new PlantUmlWrapper($this->plantUmlFormatter, $this->shellWrapper); 36 | 37 | $this->expectException(PlantUmlNotInstalledException::class); 38 | $plantUml->generate(new DependencyMap(), new SplFileInfo(__FILE__)); 39 | } 40 | 41 | public function testDetectsIfPlantUmlIsInstalled(): void 42 | { 43 | $this->shellWrapper->method('run')->willReturn(0); 44 | assertInstanceOf(PlantUmlWrapper::class, new PlantUmlWrapper($this->plantUmlFormatter, $this->shellWrapper)); 45 | } 46 | 47 | public function testGenerate(): void 48 | { 49 | $plantUml = new PlantUmlWrapper($this->plantUmlFormatter, $this->shellWrapper); 50 | $plantUml->generate(new DependencyMap(), new SplFileInfo(sys_get_temp_dir().'/dependencies.png'), true); 51 | assertFileExists(sys_get_temp_dir().'/dependencies.uml'); 52 | unlink(sys_get_temp_dir().'/dependencies.uml'); 53 | } 54 | 55 | public function testRemoveUml(): void 56 | { 57 | $plantUml = new PlantUmlWrapper($this->plantUmlFormatter, $this->shellWrapper); 58 | $plantUml->generate(new DependencyMap(), new SplFileInfo(sys_get_temp_dir().'/dependencies.png')); 59 | assertFileNotExists(sys_get_temp_dir().'/dependencies.uml'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/unit/OS/ShellWrapperTest.php: -------------------------------------------------------------------------------- 1 | run('echo')); 20 | } 21 | 22 | public function testDetectsWhenApplicationNotInstalled(): void 23 | { 24 | assertNotEquals(0, (new ShellWrapper())->run('xjcsajhckjsdfhksdf')); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/unit/Util/DependencyContainerTest.php: -------------------------------------------------------------------------------- 1 | getMethods() as $method) { 25 | if (!$method->hasReturnType()) { 26 | continue; 27 | } 28 | $methods[] = [$method->getName(), (string) $method->getReturnType()]; 29 | } 30 | return $methods; 31 | } 32 | 33 | /** 34 | * @dataProvider provideMethods 35 | * @param string $methodName 36 | * @param string $expectedReturnType 37 | */ 38 | public function testCanInstantiateAllDependencies(string $methodName, string $expectedReturnType): void 39 | { 40 | assertInstanceOf($expectedReturnType, (new DependencyContainer([]))->{$methodName}()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/Util/FunctionalTest.php: -------------------------------------------------------------------------------- 1 |