├── .github └── workflows │ ├── Deploy.yml │ └── Test.yml ├── .gitignore ├── .php_cs ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bin ├── dcv └── entrypoint.sh ├── ci └── deploy.sh ├── composer.json ├── composer.lock ├── resources ├── dependencies.png ├── display.png ├── extends.png ├── external_links.png ├── image.png ├── links.png ├── networks.png ├── ports.png └── volumes.png ├── spec ├── fetch-networks.php ├── fetch-services.php ├── fetch-volumes.php ├── fixtures │ └── read-configuration │ │ ├── invalid.yml │ │ └── valid.yml └── read-configuratoin.php └── src ├── application.php └── functions.php /.github/workflows/Deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Cache Composer packages 16 | id: composer-cache 17 | uses: actions/cache@v2 18 | with: 19 | path: vendor 20 | key: ${{ runner.os }}-node-${{ hashFiles('**/composer.lock') }} 21 | restore-keys: | 22 | ${{ runner.os }}-node- 23 | 24 | - name: Build and push Docker images 25 | uses: docker/build-push-action@v1 26 | with: 27 | username: ${{ secrets.DOCKER_USERNAME }} 28 | password: ${{ secrets.DOCKER_PASSWORD }} 29 | repository: pmsipilot/docker-compose-viz 30 | tag_with_ref: true 31 | -------------------------------------------------------------------------------- /.github/workflows/Test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Validate composer.json 19 | run: composer validate --ansi --strict 20 | 21 | - name: Cache Composer packages 22 | id: composer-cache 23 | uses: actions/cache@v2 24 | with: 25 | path: vendor 26 | key: ${{ runner.os }}-node-${{ hashFiles('**/composer.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-node- 29 | 30 | - name: Install dependencies 31 | if: steps.composer-cache.outputs.cache-hit != 'true' 32 | run: composer install --prefer-dist --no-progress --no-suggest 33 | 34 | - name: Unit tests 35 | run: composer run ut 36 | 37 | - name: Coding style 38 | run: composer run cst 39 | 40 | - name: Build and push Docker images 41 | uses: docker/build-push-action@v1 42 | with: 43 | push: false 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | docker.lock 3 | bin/ 4 | !bin/dcv 5 | !bin/entrypoint.sh 6 | .cache/ 7 | .php_cs.cache 8 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | in(__DIR__.DIRECTORY_SEPARATOR.'src') 5 | ->in(__DIR__.DIRECTORY_SEPARATOR.'spec') 6 | ; 7 | 8 | return (new PhpCsFixer\Config()) 9 | ->setRules([ 10 | '@PSR2' => true, 11 | '@Symfony' => true, 12 | 'array_syntax' => ['syntax' => 'short'], 13 | 'no_useless_else' => true, 14 | 'no_useless_return' => true, 15 | 'ordered_class_elements' => true, 16 | ]) 17 | ->setFinder($finder) 18 | ; 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `1.2.0` (unreleased) 2 | 3 | * Add a logger and enable `-v` and `-vv` options 4 | * Add the `--no-networks` and `--no-ports` options to avoid rendering networks and ports 5 | * Add the `--background` option to set the graph's background color 6 | * Versions correctly merged and checked 7 | 8 | # `1.1.0` 9 | 10 | * Display `depends_on` conditions 11 | * Handle conditions in `depends_on` 12 | * Automatically load override file if it exists or ignore it using `--ignore-override` 13 | 14 | # `1.0.0` 15 | 16 | * Avoid duplicating edges when there is multiple extended services 17 | * Display extended services as components with inverted arrows 18 | * Display services as components 19 | * Display volumes as folders 20 | * Display ports as circles 21 | * Display networks as pentagon 22 | * Display service links as plain arrows 23 | * Display service dependencies as dotted arrows 24 | * Display volume links as dashed arrows 25 | * Display external resources as grayed items 26 | * Render graph as PNG (`image` renderer) 27 | * Render graph as dot file (`dot` renderer) 28 | * Open graph as PNG image (`display` renderer) 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-alpine as builder 2 | 3 | COPY composer.json /dcv/composer.json 4 | COPY composer.lock /dcv/composer.lock 5 | 6 | WORKDIR /dcv 7 | 8 | RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \ 9 | php composer-setup.php && \ 10 | php -r "unlink('composer-setup.php');" && \ 11 | php composer.phar install --prefer-dist 12 | 13 | FROM php:7.4-alpine 14 | 15 | RUN apk update && \ 16 | apk add graphviz ttf-dejavu && \ 17 | rm -rf \ 18 | /var/cache/apk/* \ 19 | /tmp/* 20 | 21 | COPY bin/ /dcv/bin 22 | COPY src/ /dcv/src 23 | COPY --from=builder /dcv/vendor /dcv/vendor 24 | 25 | RUN chmod +x /dcv/bin/dcv 26 | 27 | RUN addgroup dcv && \ 28 | adduser -D -G dcv -s /bin/bash -g "docker-compose-viz" -h /input dcv 29 | 30 | USER dcv 31 | VOLUME /input 32 | WORKDIR /input 33 | 34 | ENTRYPOINT ["/dcv/bin/entrypoint.sh"] 35 | CMD ["render", "-m", "image", "-f"] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2020 PMSIpilot 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 7 | persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DCV_IMAGE_NAME=pmsipilot/docker-compose-viz 2 | 3 | COMPOSER ?= composer 4 | COMPOSERFLAGS ?= 5 | DOCKER ?= docker 6 | PHP ?= php 7 | 8 | .PHONY: clean docker test unit cs fix-cs 9 | 10 | docker: docker.lock 11 | 12 | test: vendor unit cs 13 | 14 | unit: vendor 15 | $(COMPOSER) run ut 16 | 17 | cs: 18 | $(COMPOSER) run cst 19 | 20 | fix-cs: 21 | $(COMPOSER) run cs 22 | 23 | clean: 24 | rm -rf vendor/ 25 | 26 | docker.lock: Dockerfile bin/entrypoint.sh vendor src/application.php src/functions.php 27 | $(DOCKER) build -t $(DCV_IMAGE_NAME) . 28 | touch docker.lock 29 | 30 | vendor: composer.lock 31 | $(COMPOSER) install --prefer-dist 32 | 33 | composer.lock: composer.json 34 | $(COMPOSER) update $(COMPOSERFLAGS) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `docker-compose-viz` 2 | 3 | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/pmsipilot/docker-compose-viz.svg)](http://isitmaintained.com/project/pmsipilot/docker-compose-viz "Average time to resolve an issue") 4 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/pmsipilot/docker-compose-viz.svg)](http://isitmaintained.com/project/pmsipilot/docker-compose-viz "Percentage of issues still open") 5 | [![Docker Stars](https://img.shields.io/docker/stars/pmsipilot/docker-compose-viz.svg?style=flat)](https://hub.docker.com/r/pmsipilot/docker-compose-viz/) 6 | [![Docker Pulls](https://img.shields.io/docker/pulls/pmsipilot/docker-compose-viz.svg?style=flat)](https://hub.docker.com/r/pmsipilot/docker-compose-viz/) 7 | 8 | ## How to use 9 | 10 | ### Docker 11 | 12 | Considering the current working directory is where your `docker-compose.yml` file is located: 13 | 14 | ```sh 15 | docker run --rm -it --name dcv -v $(pwd):/input pmsipilot/docker-compose-viz render -m image docker-compose.yml 16 | ``` 17 | 18 | ```powershell 19 | # PowerShell 20 | docker run --rm -it --name dcv -v ${pwd}:/input pmsipilot/docker-compose-viz render -m image docker-compose.yml 21 | ``` 22 | 23 | This will generate the `docker-compose.png` file in the current working directory. 24 | 25 | ### PHP 26 | 27 | Before you start, make sure you have: 28 | 29 | * [Composer](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx) installed, 30 | * [PHP 7.2](http://php.net/downloads.php#v7.2.32) (at least) installed, 31 | * GraphViz installed (see below for a guide on how to install it) 32 | 33 | #### Self-Compiled 34 | 35 | ```sh 36 | git clone https://github.com/pmsipilot/docker-compose-viz.git 37 | 38 | make vendor 39 | ``` 40 | 41 | #### Via Composer 42 | 43 | ```sh 44 | composer install --prefer-dist 45 | 46 | bin/dcv 47 | ``` 48 | 49 | #### Install GraphViz 50 | 51 | * On MacOS: 52 | 53 | ```sh 54 | brew install graphviz 55 | ``` 56 | 57 | * On Debian: 58 | 59 | ```sh 60 | sudo apt-get install graphviz 61 | ``` 62 | 63 | ## Usage 64 | 65 | ```sh 66 | Usage: 67 | render [options] [--] [] 68 | 69 | Arguments: 70 | input-file Path to a docker compose file [default: "./docker-compose.yml"] 71 | 72 | Options: 73 | --override=OVERRIDE Tag of the override file to use [default: "override"] 74 | -o, --output-file=OUTPUT-FILE Path to a output file (Only for "dot" and "image" output format) [default: "./docker-compose.dot" or "./docker-compose.png"] 75 | -m, --output-format=OUTPUT-FORMAT Output format (one of: "dot", "image", "display") [default: "display"] 76 | --only=ONLY Display a graph only for a given services (multiple values allowed) 77 | -f, --force Overwrites output file if it already exists 78 | --no-volumes Do not display volumes 79 | -r, --horizontal Display a horizontal graph 80 | --ignore-override Ignore override file 81 | ``` 82 | 83 | ## How to read the graph 84 | 85 | ### Links 86 | 87 | Links (from `services..links`) are displayed as plain arrows pointing to the service that declares the link: 88 | 89 | ![links](resources/links.png) 90 | 91 | If we look at the link between `mysql` and `ambassador`, it reads as follow: "`mysql` is known as `mysql` in `ambassador`." 92 | If we look at the link between `ambassador` and `logs`, it reads as follow: "`ambassador` is known as `logstash` in `logs`." 93 | 94 | External links are displayed using the same shapes but are grayed: 95 | 96 | ![external_links](resources/external_links.png) 97 | 98 | ### Volumes 99 | 100 | Volumes (from `services..volumes_from`) are displayed as dashed arrows pointing to the service that uses the volumes: 101 | 102 | ![volumes](resources/volumes.png) 103 | 104 | If we look at the link between `logs` and `api`, it reads as follow: "`api` uses volumes from `logs`." 105 | 106 | Volumes (from `services..volumes`) are displayed as folders with the host directory as label and are linked to the service that uses them dashed arrows. 107 | 108 | If we look at the link between `./api` and `api`, it reads as follow: "the host directory `./api`is mounted as a read-write folder on `/src` in `api`." Bidirectional arrows mean the directory is writable from the container. 109 | 110 | If we look at the link between `./etc/api/php-fpm.d` and `api`, it reads as follow: "the host directory `./etc/api/php-fpm.d`is mounted as a read-only folder on `/usr/local/etc/php-fpm.d` in `api`." Unidirectional arrows mean the directory is not writable from the container. 111 | 112 | ### Dependencies 113 | 114 | Dependencies (from `services..depends_on`) are displayed as dotted arrows pointing to the service that declares the dependencies: 115 | 116 | ![dependencies](resources/dependencies.png) 117 | 118 | If we look at the link between `mysql` and `logs`, it reads as follow: "`mysql` depends on `logs`." 119 | 120 | ### Ports 121 | 122 | Ports (from `services..ports`) are displayed as circle and are linked to containers using plain arrows pointing to the service that declares the ports: 123 | 124 | ![ports](resources/ports.png) 125 | 126 | If we look at the link between port `2480` and `orientdb`, it reads as follow: "traffic coming to host port `2480` will be routed to port `2480` of `orientdb`." 127 | If we look at the link between port `2580` and `elk`, it reads as follow: "traffix coming to host port `2580` will be routed to port `80` of `elk`." 128 | 129 | ### Extends 130 | 131 | Extended services (from `services..extends`) are displayed as components (just like normal services). The links between them and the extending services are 132 | displayed as inverted arrows: 133 | 134 | ![extends](resources/extends.png) 135 | 136 | If we look at the link between `mysql` and `db`, it reads as follow: "`mysql` extends service `db`". 137 | 138 | ### Networks 139 | 140 | Networks (from `networks.`) are displayed as pentagons. The links between them and services are displayed as plain arrows pointing to the network: 141 | 142 | ![networks](resources/networks.png) 143 | 144 | If we look at the link between `mysql` and the `global` network, it reads as follow: "`mysql` is known as `mysql`, `db` and `reldb` in the `global` network. 145 | 146 | The `legacy` network is an external so it's displayed as a grayed pentagon. 147 | 148 | ## Examples 149 | 150 | ### `dot` renderer 151 | 152 | ```dot 153 | digraph G { 154 | graph [pad=0.5] 155 | "front" [shape="component"] 156 | "http" [shape="component"] 157 | 2380 [shape="circle"] 158 | "ambassador" [shape="component"] 159 | "mysql" [shape="component"] 160 | "orientdb" [shape="component"] 161 | "elk" [shape="component"] 162 | "api" [shape="component"] 163 | "piwik" [shape="component"] 164 | "logs" [shape="component"] 165 | "html" [shape="component"] 166 | 2580 [shape="circle"] 167 | 2480 [shape="circle"] 168 | "http" -> "front" [style="solid"] 169 | 2380 -> "front" [style="solid" label=80] 170 | "mysql" -> "ambassador" [style="solid"] 171 | "orientdb" -> "ambassador" [style="solid"] 172 | "elk" -> "ambassador" [style="solid"] 173 | "api" -> "http" [style="solid"] 174 | "piwik" -> "http" [style="solid"] 175 | "logs" -> "http" [style="dashed"] 176 | "piwik" -> "http" [style="dashed"] 177 | "html" -> "http" [style="dashed"] 178 | "ambassador" -> "api" [style="solid" label="graphdb"] 179 | "ambassador" -> "api" [style="solid" label="reldb"] 180 | "logs" -> "api" [style="dashed"] 181 | "ambassador" -> "logs" [style="solid" label="logstash"] 182 | 2580 -> "elk" [style="solid" label=80] 183 | "ambassador" -> "piwik" [style="solid" label="db"] 184 | 2480 -> "orientdb" [style="solid"] 185 | } 186 | ``` 187 | 188 | ### `image` renderer 189 | 190 | ![image renderer](resources/image.png) 191 | 192 | ### `display` renderer 193 | 194 | ![display renderer](resources/display.png) 195 | 196 | ### Troubleshooting 197 | 198 | #### Getting "failed to open stream: Permission denied"? 199 | 200 | Make sure the target directory is writeable by the user in the Docker container. 201 | Or create a writeable directory first. See [workaround #41](https://github.com/pmsipilot/docker-compose-viz/issues/41#issuecomment-483384999) 202 | 203 | ## License 204 | 205 | The MIT License (MIT) 206 | Copyright ® 2020 PMSIpilot 207 | -------------------------------------------------------------------------------- /bin/dcv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 26 | -------------------------------------------------------------------------------- /bin/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | CURRENT_DIR=$(dirname $0) 6 | 7 | if [ "$1" = "render" ] 8 | then 9 | $CURRENT_DIR/dcv "$@" 10 | else 11 | exec "$@" 12 | fi 13 | -------------------------------------------------------------------------------- /ci/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export REPO=pmsipilot/docker-compose-viz 4 | export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` 5 | 6 | git reset --hard 7 | git clean -dfx 8 | composer install --no-dev --prefer-dist --classmap-authoritative 9 | 10 | docker login -u $DOCKER_USER -p $DOCKER_PASS 11 | docker build -f Dockerfile -t $REPO:$TAG . 12 | docker push $REPO 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pmsipilot/docker-compose-viz", 3 | "description": "Docker compose graph visualization", 4 | "require": { 5 | "php": "^7.2", 6 | "symfony/yaml": "^3.1 || ^4", 7 | "symfony/console": "^3.1", 8 | "clue/graph": "^0.9", 9 | "graphp/graphviz": "^0.2" 10 | }, 11 | "require-dev": { 12 | "friendsofphp/php-cs-fixer": "^2", 13 | "kahlan/kahlan": "^4.7" 14 | }, 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Julien Bianchi", 19 | "email": "julien.bianchi@pmsipilot.com" 20 | } 21 | ], 22 | "autoload": { 23 | "files": ["src/functions.php"], 24 | "psr-4": { 25 | "PMSIpilot\\DockerComposeViz\\": "src/" 26 | } 27 | }, 28 | "scripts": { 29 | "cs": "php-cs-fixer fix", 30 | "cst": "php-cs-fixer fix --dry-run", 31 | "ut": "kahlan --grep='*.php' --reporter=verbose --persistent=false" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "883183cc51537213776e61c66e969e5a", 8 | "packages": [ 9 | { 10 | "name": "clue/graph", 11 | "version": "v0.9.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/graphp/graph.git", 15 | "reference": "07ce1e2f6d5be2ff600ce13660b25f25cf928c66" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/graphp/graph/zipball/07ce1e2f6d5be2ff600ce13660b25f25cf928c66", 20 | "reference": "07ce1e2f6d5be2ff600ce13660b25f25cf928c66", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": "^7.0 || ^5.3" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" 28 | }, 29 | "suggest": { 30 | "graphp/algorithms": "Common graph algorithms, such as Dijkstra and Moore-Bellman-Ford (shortest path), minimum spanning tree (MST), Kruskal, Prim and many more..", 31 | "graphp/graphviz": "GraphViz graph drawing / DOT output" 32 | }, 33 | "type": "library", 34 | "autoload": { 35 | "psr-4": { 36 | "Fhaculty\\Graph\\": "src/" 37 | } 38 | }, 39 | "notification-url": "https://packagist.org/downloads/", 40 | "license": [ 41 | "MIT" 42 | ], 43 | "description": "GraPHP is the mathematical graph/network library written in PHP.", 44 | "homepage": "https://github.com/graphp/graph", 45 | "keywords": [ 46 | "edge", 47 | "graph", 48 | "mathematical", 49 | "network", 50 | "vertex" 51 | ], 52 | "time": "2019-10-02T09:10:26+00:00" 53 | }, 54 | { 55 | "name": "graphp/graphviz", 56 | "version": "v0.2.2", 57 | "source": { 58 | "type": "git", 59 | "url": "https://github.com/graphp/graphviz.git", 60 | "reference": "5cc4466223ca46fffa196d1e762fae164319c229" 61 | }, 62 | "dist": { 63 | "type": "zip", 64 | "url": "https://api.github.com/repos/graphp/graphviz/zipball/5cc4466223ca46fffa196d1e762fae164319c229", 65 | "reference": "5cc4466223ca46fffa196d1e762fae164319c229", 66 | "shasum": "" 67 | }, 68 | "require": { 69 | "clue/graph": "~0.9.0|~0.8.0", 70 | "php": ">=5.3.0" 71 | }, 72 | "require-dev": { 73 | "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" 74 | }, 75 | "type": "library", 76 | "autoload": { 77 | "psr-4": { 78 | "Graphp\\GraphViz\\": "src/" 79 | } 80 | }, 81 | "notification-url": "https://packagist.org/downloads/", 82 | "license": [ 83 | "MIT" 84 | ], 85 | "description": "GraphViz graph drawing for the mathematical graph/network library GraPHP.", 86 | "homepage": "https://github.com/graphp/graphviz", 87 | "keywords": [ 88 | "dot output", 89 | "graph drawing", 90 | "graph image", 91 | "graphp", 92 | "graphviz" 93 | ], 94 | "time": "2019-10-04T13:30:55+00:00" 95 | }, 96 | { 97 | "name": "psr/log", 98 | "version": "1.1.3", 99 | "source": { 100 | "type": "git", 101 | "url": "https://github.com/php-fig/log.git", 102 | "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" 103 | }, 104 | "dist": { 105 | "type": "zip", 106 | "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", 107 | "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", 108 | "shasum": "" 109 | }, 110 | "require": { 111 | "php": ">=5.3.0" 112 | }, 113 | "type": "library", 114 | "extra": { 115 | "branch-alias": { 116 | "dev-master": "1.1.x-dev" 117 | } 118 | }, 119 | "autoload": { 120 | "psr-4": { 121 | "Psr\\Log\\": "Psr/Log/" 122 | } 123 | }, 124 | "notification-url": "https://packagist.org/downloads/", 125 | "license": [ 126 | "MIT" 127 | ], 128 | "authors": [ 129 | { 130 | "name": "PHP-FIG", 131 | "homepage": "http://www.php-fig.org/" 132 | } 133 | ], 134 | "description": "Common interface for logging libraries", 135 | "homepage": "https://github.com/php-fig/log", 136 | "keywords": [ 137 | "log", 138 | "psr", 139 | "psr-3" 140 | ], 141 | "time": "2020-03-23T09:12:05+00:00" 142 | }, 143 | { 144 | "name": "symfony/console", 145 | "version": "v3.4.42", 146 | "source": { 147 | "type": "git", 148 | "url": "https://github.com/symfony/console.git", 149 | "reference": "bfe29ead7e7b1cc9ce74c6a40d06ad1f96fced13" 150 | }, 151 | "dist": { 152 | "type": "zip", 153 | "url": "https://api.github.com/repos/symfony/console/zipball/bfe29ead7e7b1cc9ce74c6a40d06ad1f96fced13", 154 | "reference": "bfe29ead7e7b1cc9ce74c6a40d06ad1f96fced13", 155 | "shasum": "" 156 | }, 157 | "require": { 158 | "php": "^5.5.9|>=7.0.8", 159 | "symfony/debug": "~2.8|~3.0|~4.0", 160 | "symfony/polyfill-mbstring": "~1.0" 161 | }, 162 | "conflict": { 163 | "symfony/dependency-injection": "<3.4", 164 | "symfony/process": "<3.3" 165 | }, 166 | "provide": { 167 | "psr/log-implementation": "1.0" 168 | }, 169 | "require-dev": { 170 | "psr/log": "~1.0", 171 | "symfony/config": "~3.3|~4.0", 172 | "symfony/dependency-injection": "~3.4|~4.0", 173 | "symfony/event-dispatcher": "~2.8|~3.0|~4.0", 174 | "symfony/lock": "~3.4|~4.0", 175 | "symfony/process": "~3.3|~4.0" 176 | }, 177 | "suggest": { 178 | "psr/log": "For using the console logger", 179 | "symfony/event-dispatcher": "", 180 | "symfony/lock": "", 181 | "symfony/process": "" 182 | }, 183 | "type": "library", 184 | "extra": { 185 | "branch-alias": { 186 | "dev-master": "3.4-dev" 187 | } 188 | }, 189 | "autoload": { 190 | "psr-4": { 191 | "Symfony\\Component\\Console\\": "" 192 | }, 193 | "exclude-from-classmap": [ 194 | "/Tests/" 195 | ] 196 | }, 197 | "notification-url": "https://packagist.org/downloads/", 198 | "license": [ 199 | "MIT" 200 | ], 201 | "authors": [ 202 | { 203 | "name": "Fabien Potencier", 204 | "email": "fabien@symfony.com" 205 | }, 206 | { 207 | "name": "Symfony Community", 208 | "homepage": "https://symfony.com/contributors" 209 | } 210 | ], 211 | "description": "Symfony Console Component", 212 | "homepage": "https://symfony.com", 213 | "time": "2020-05-30T18:58:05+00:00" 214 | }, 215 | { 216 | "name": "symfony/debug", 217 | "version": "v4.4.10", 218 | "source": { 219 | "type": "git", 220 | "url": "https://github.com/symfony/debug.git", 221 | "reference": "28f92d08bb6d1fddf8158e02c194ad43870007e6" 222 | }, 223 | "dist": { 224 | "type": "zip", 225 | "url": "https://api.github.com/repos/symfony/debug/zipball/28f92d08bb6d1fddf8158e02c194ad43870007e6", 226 | "reference": "28f92d08bb6d1fddf8158e02c194ad43870007e6", 227 | "shasum": "" 228 | }, 229 | "require": { 230 | "php": ">=7.1.3", 231 | "psr/log": "~1.0", 232 | "symfony/polyfill-php80": "^1.15" 233 | }, 234 | "conflict": { 235 | "symfony/http-kernel": "<3.4" 236 | }, 237 | "require-dev": { 238 | "symfony/http-kernel": "^3.4|^4.0|^5.0" 239 | }, 240 | "type": "library", 241 | "extra": { 242 | "branch-alias": { 243 | "dev-master": "4.4-dev" 244 | } 245 | }, 246 | "autoload": { 247 | "psr-4": { 248 | "Symfony\\Component\\Debug\\": "" 249 | }, 250 | "exclude-from-classmap": [ 251 | "/Tests/" 252 | ] 253 | }, 254 | "notification-url": "https://packagist.org/downloads/", 255 | "license": [ 256 | "MIT" 257 | ], 258 | "authors": [ 259 | { 260 | "name": "Fabien Potencier", 261 | "email": "fabien@symfony.com" 262 | }, 263 | { 264 | "name": "Symfony Community", 265 | "homepage": "https://symfony.com/contributors" 266 | } 267 | ], 268 | "description": "Symfony Debug Component", 269 | "homepage": "https://symfony.com", 270 | "time": "2020-05-24T08:33:35+00:00" 271 | }, 272 | { 273 | "name": "symfony/polyfill-ctype", 274 | "version": "v1.18.0", 275 | "source": { 276 | "type": "git", 277 | "url": "https://github.com/symfony/polyfill-ctype.git", 278 | "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" 279 | }, 280 | "dist": { 281 | "type": "zip", 282 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", 283 | "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", 284 | "shasum": "" 285 | }, 286 | "require": { 287 | "php": ">=5.3.3" 288 | }, 289 | "suggest": { 290 | "ext-ctype": "For best performance" 291 | }, 292 | "type": "library", 293 | "extra": { 294 | "branch-alias": { 295 | "dev-master": "1.18-dev" 296 | }, 297 | "thanks": { 298 | "name": "symfony/polyfill", 299 | "url": "https://github.com/symfony/polyfill" 300 | } 301 | }, 302 | "autoload": { 303 | "psr-4": { 304 | "Symfony\\Polyfill\\Ctype\\": "" 305 | }, 306 | "files": [ 307 | "bootstrap.php" 308 | ] 309 | }, 310 | "notification-url": "https://packagist.org/downloads/", 311 | "license": [ 312 | "MIT" 313 | ], 314 | "authors": [ 315 | { 316 | "name": "Gert de Pagter", 317 | "email": "BackEndTea@gmail.com" 318 | }, 319 | { 320 | "name": "Symfony Community", 321 | "homepage": "https://symfony.com/contributors" 322 | } 323 | ], 324 | "description": "Symfony polyfill for ctype functions", 325 | "homepage": "https://symfony.com", 326 | "keywords": [ 327 | "compatibility", 328 | "ctype", 329 | "polyfill", 330 | "portable" 331 | ], 332 | "time": "2020-07-14T12:35:20+00:00" 333 | }, 334 | { 335 | "name": "symfony/polyfill-mbstring", 336 | "version": "v1.18.0", 337 | "source": { 338 | "type": "git", 339 | "url": "https://github.com/symfony/polyfill-mbstring.git", 340 | "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" 341 | }, 342 | "dist": { 343 | "type": "zip", 344 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", 345 | "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", 346 | "shasum": "" 347 | }, 348 | "require": { 349 | "php": ">=5.3.3" 350 | }, 351 | "suggest": { 352 | "ext-mbstring": "For best performance" 353 | }, 354 | "type": "library", 355 | "extra": { 356 | "branch-alias": { 357 | "dev-master": "1.18-dev" 358 | }, 359 | "thanks": { 360 | "name": "symfony/polyfill", 361 | "url": "https://github.com/symfony/polyfill" 362 | } 363 | }, 364 | "autoload": { 365 | "psr-4": { 366 | "Symfony\\Polyfill\\Mbstring\\": "" 367 | }, 368 | "files": [ 369 | "bootstrap.php" 370 | ] 371 | }, 372 | "notification-url": "https://packagist.org/downloads/", 373 | "license": [ 374 | "MIT" 375 | ], 376 | "authors": [ 377 | { 378 | "name": "Nicolas Grekas", 379 | "email": "p@tchwork.com" 380 | }, 381 | { 382 | "name": "Symfony Community", 383 | "homepage": "https://symfony.com/contributors" 384 | } 385 | ], 386 | "description": "Symfony polyfill for the Mbstring extension", 387 | "homepage": "https://symfony.com", 388 | "keywords": [ 389 | "compatibility", 390 | "mbstring", 391 | "polyfill", 392 | "portable", 393 | "shim" 394 | ], 395 | "time": "2020-07-14T12:35:20+00:00" 396 | }, 397 | { 398 | "name": "symfony/polyfill-php80", 399 | "version": "v1.18.0", 400 | "source": { 401 | "type": "git", 402 | "url": "https://github.com/symfony/polyfill-php80.git", 403 | "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" 404 | }, 405 | "dist": { 406 | "type": "zip", 407 | "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", 408 | "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", 409 | "shasum": "" 410 | }, 411 | "require": { 412 | "php": ">=7.0.8" 413 | }, 414 | "type": "library", 415 | "extra": { 416 | "branch-alias": { 417 | "dev-master": "1.18-dev" 418 | }, 419 | "thanks": { 420 | "name": "symfony/polyfill", 421 | "url": "https://github.com/symfony/polyfill" 422 | } 423 | }, 424 | "autoload": { 425 | "psr-4": { 426 | "Symfony\\Polyfill\\Php80\\": "" 427 | }, 428 | "files": [ 429 | "bootstrap.php" 430 | ], 431 | "classmap": [ 432 | "Resources/stubs" 433 | ] 434 | }, 435 | "notification-url": "https://packagist.org/downloads/", 436 | "license": [ 437 | "MIT" 438 | ], 439 | "authors": [ 440 | { 441 | "name": "Ion Bazan", 442 | "email": "ion.bazan@gmail.com" 443 | }, 444 | { 445 | "name": "Nicolas Grekas", 446 | "email": "p@tchwork.com" 447 | }, 448 | { 449 | "name": "Symfony Community", 450 | "homepage": "https://symfony.com/contributors" 451 | } 452 | ], 453 | "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", 454 | "homepage": "https://symfony.com", 455 | "keywords": [ 456 | "compatibility", 457 | "polyfill", 458 | "portable", 459 | "shim" 460 | ], 461 | "time": "2020-07-14T12:35:20+00:00" 462 | }, 463 | { 464 | "name": "symfony/yaml", 465 | "version": "v4.4.10", 466 | "source": { 467 | "type": "git", 468 | "url": "https://github.com/symfony/yaml.git", 469 | "reference": "c2d2cc66e892322cfcc03f8f12f8340dbd7a3f8a" 470 | }, 471 | "dist": { 472 | "type": "zip", 473 | "url": "https://api.github.com/repos/symfony/yaml/zipball/c2d2cc66e892322cfcc03f8f12f8340dbd7a3f8a", 474 | "reference": "c2d2cc66e892322cfcc03f8f12f8340dbd7a3f8a", 475 | "shasum": "" 476 | }, 477 | "require": { 478 | "php": ">=7.1.3", 479 | "symfony/polyfill-ctype": "~1.8" 480 | }, 481 | "conflict": { 482 | "symfony/console": "<3.4" 483 | }, 484 | "require-dev": { 485 | "symfony/console": "^3.4|^4.0|^5.0" 486 | }, 487 | "suggest": { 488 | "symfony/console": "For validating YAML files using the lint command" 489 | }, 490 | "type": "library", 491 | "extra": { 492 | "branch-alias": { 493 | "dev-master": "4.4-dev" 494 | } 495 | }, 496 | "autoload": { 497 | "psr-4": { 498 | "Symfony\\Component\\Yaml\\": "" 499 | }, 500 | "exclude-from-classmap": [ 501 | "/Tests/" 502 | ] 503 | }, 504 | "notification-url": "https://packagist.org/downloads/", 505 | "license": [ 506 | "MIT" 507 | ], 508 | "authors": [ 509 | { 510 | "name": "Fabien Potencier", 511 | "email": "fabien@symfony.com" 512 | }, 513 | { 514 | "name": "Symfony Community", 515 | "homepage": "https://symfony.com/contributors" 516 | } 517 | ], 518 | "description": "Symfony Yaml Component", 519 | "homepage": "https://symfony.com", 520 | "time": "2020-05-20T08:37:50+00:00" 521 | } 522 | ], 523 | "packages-dev": [ 524 | { 525 | "name": "composer/semver", 526 | "version": "1.5.1", 527 | "source": { 528 | "type": "git", 529 | "url": "https://github.com/composer/semver.git", 530 | "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de" 531 | }, 532 | "dist": { 533 | "type": "zip", 534 | "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de", 535 | "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de", 536 | "shasum": "" 537 | }, 538 | "require": { 539 | "php": "^5.3.2 || ^7.0" 540 | }, 541 | "require-dev": { 542 | "phpunit/phpunit": "^4.5 || ^5.0.5" 543 | }, 544 | "type": "library", 545 | "extra": { 546 | "branch-alias": { 547 | "dev-master": "1.x-dev" 548 | } 549 | }, 550 | "autoload": { 551 | "psr-4": { 552 | "Composer\\Semver\\": "src" 553 | } 554 | }, 555 | "notification-url": "https://packagist.org/downloads/", 556 | "license": [ 557 | "MIT" 558 | ], 559 | "authors": [ 560 | { 561 | "name": "Nils Adermann", 562 | "email": "naderman@naderman.de", 563 | "homepage": "http://www.naderman.de" 564 | }, 565 | { 566 | "name": "Jordi Boggiano", 567 | "email": "j.boggiano@seld.be", 568 | "homepage": "http://seld.be" 569 | }, 570 | { 571 | "name": "Rob Bast", 572 | "email": "rob.bast@gmail.com", 573 | "homepage": "http://robbast.nl" 574 | } 575 | ], 576 | "description": "Semver library that offers utilities, version constraint parsing and validation.", 577 | "keywords": [ 578 | "semantic", 579 | "semver", 580 | "validation", 581 | "versioning" 582 | ], 583 | "time": "2020-01-13T12:06:48+00:00" 584 | }, 585 | { 586 | "name": "composer/xdebug-handler", 587 | "version": "1.4.2", 588 | "source": { 589 | "type": "git", 590 | "url": "https://github.com/composer/xdebug-handler.git", 591 | "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51" 592 | }, 593 | "dist": { 594 | "type": "zip", 595 | "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", 596 | "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", 597 | "shasum": "" 598 | }, 599 | "require": { 600 | "php": "^5.3.2 || ^7.0 || ^8.0", 601 | "psr/log": "^1.0" 602 | }, 603 | "require-dev": { 604 | "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" 605 | }, 606 | "type": "library", 607 | "autoload": { 608 | "psr-4": { 609 | "Composer\\XdebugHandler\\": "src" 610 | } 611 | }, 612 | "notification-url": "https://packagist.org/downloads/", 613 | "license": [ 614 | "MIT" 615 | ], 616 | "authors": [ 617 | { 618 | "name": "John Stevenson", 619 | "email": "john-stevenson@blueyonder.co.uk" 620 | } 621 | ], 622 | "description": "Restarts a process without Xdebug.", 623 | "keywords": [ 624 | "Xdebug", 625 | "performance" 626 | ], 627 | "time": "2020-06-04T11:16:35+00:00" 628 | }, 629 | { 630 | "name": "doctrine/annotations", 631 | "version": "1.10.3", 632 | "source": { 633 | "type": "git", 634 | "url": "https://github.com/doctrine/annotations.git", 635 | "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d" 636 | }, 637 | "dist": { 638 | "type": "zip", 639 | "url": "https://api.github.com/repos/doctrine/annotations/zipball/5db60a4969eba0e0c197a19c077780aadbc43c5d", 640 | "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d", 641 | "shasum": "" 642 | }, 643 | "require": { 644 | "doctrine/lexer": "1.*", 645 | "ext-tokenizer": "*", 646 | "php": "^7.1 || ^8.0" 647 | }, 648 | "require-dev": { 649 | "doctrine/cache": "1.*", 650 | "phpunit/phpunit": "^7.5" 651 | }, 652 | "type": "library", 653 | "extra": { 654 | "branch-alias": { 655 | "dev-master": "1.9.x-dev" 656 | } 657 | }, 658 | "autoload": { 659 | "psr-4": { 660 | "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" 661 | } 662 | }, 663 | "notification-url": "https://packagist.org/downloads/", 664 | "license": [ 665 | "MIT" 666 | ], 667 | "authors": [ 668 | { 669 | "name": "Guilherme Blanco", 670 | "email": "guilhermeblanco@gmail.com" 671 | }, 672 | { 673 | "name": "Roman Borschel", 674 | "email": "roman@code-factory.org" 675 | }, 676 | { 677 | "name": "Benjamin Eberlei", 678 | "email": "kontakt@beberlei.de" 679 | }, 680 | { 681 | "name": "Jonathan Wage", 682 | "email": "jonwage@gmail.com" 683 | }, 684 | { 685 | "name": "Johannes Schmitt", 686 | "email": "schmittjoh@gmail.com" 687 | } 688 | ], 689 | "description": "Docblock Annotations Parser", 690 | "homepage": "http://www.doctrine-project.org", 691 | "keywords": [ 692 | "annotations", 693 | "docblock", 694 | "parser" 695 | ], 696 | "time": "2020-05-25T17:24:27+00:00" 697 | }, 698 | { 699 | "name": "doctrine/lexer", 700 | "version": "1.2.1", 701 | "source": { 702 | "type": "git", 703 | "url": "https://github.com/doctrine/lexer.git", 704 | "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" 705 | }, 706 | "dist": { 707 | "type": "zip", 708 | "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", 709 | "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", 710 | "shasum": "" 711 | }, 712 | "require": { 713 | "php": "^7.2 || ^8.0" 714 | }, 715 | "require-dev": { 716 | "doctrine/coding-standard": "^6.0", 717 | "phpstan/phpstan": "^0.11.8", 718 | "phpunit/phpunit": "^8.2" 719 | }, 720 | "type": "library", 721 | "extra": { 722 | "branch-alias": { 723 | "dev-master": "1.2.x-dev" 724 | } 725 | }, 726 | "autoload": { 727 | "psr-4": { 728 | "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" 729 | } 730 | }, 731 | "notification-url": "https://packagist.org/downloads/", 732 | "license": [ 733 | "MIT" 734 | ], 735 | "authors": [ 736 | { 737 | "name": "Guilherme Blanco", 738 | "email": "guilhermeblanco@gmail.com" 739 | }, 740 | { 741 | "name": "Roman Borschel", 742 | "email": "roman@code-factory.org" 743 | }, 744 | { 745 | "name": "Johannes Schmitt", 746 | "email": "schmittjoh@gmail.com" 747 | } 748 | ], 749 | "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", 750 | "homepage": "https://www.doctrine-project.org/projects/lexer.html", 751 | "keywords": [ 752 | "annotations", 753 | "docblock", 754 | "lexer", 755 | "parser", 756 | "php" 757 | ], 758 | "time": "2020-05-25T17:44:05+00:00" 759 | }, 760 | { 761 | "name": "friendsofphp/php-cs-fixer", 762 | "version": "v2.16.4", 763 | "source": { 764 | "type": "git", 765 | "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", 766 | "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13" 767 | }, 768 | "dist": { 769 | "type": "zip", 770 | "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1023c3458137ab052f6ff1e09621a721bfdeca13", 771 | "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13", 772 | "shasum": "" 773 | }, 774 | "require": { 775 | "composer/semver": "^1.4", 776 | "composer/xdebug-handler": "^1.2", 777 | "doctrine/annotations": "^1.2", 778 | "ext-json": "*", 779 | "ext-tokenizer": "*", 780 | "php": "^5.6 || ^7.0", 781 | "php-cs-fixer/diff": "^1.3", 782 | "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0", 783 | "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", 784 | "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", 785 | "symfony/finder": "^3.0 || ^4.0 || ^5.0", 786 | "symfony/options-resolver": "^3.0 || ^4.0 || ^5.0", 787 | "symfony/polyfill-php70": "^1.0", 788 | "symfony/polyfill-php72": "^1.4", 789 | "symfony/process": "^3.0 || ^4.0 || ^5.0", 790 | "symfony/stopwatch": "^3.0 || ^4.0 || ^5.0" 791 | }, 792 | "require-dev": { 793 | "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", 794 | "justinrainbow/json-schema": "^5.0", 795 | "keradus/cli-executor": "^1.2", 796 | "mikey179/vfsstream": "^1.6", 797 | "php-coveralls/php-coveralls": "^2.1", 798 | "php-cs-fixer/accessible-object": "^1.0", 799 | "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.1", 800 | "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", 801 | "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", 802 | "phpunitgoodpractices/traits": "^1.8", 803 | "symfony/phpunit-bridge": "^5.1", 804 | "symfony/yaml": "^3.0 || ^4.0 || ^5.0" 805 | }, 806 | "suggest": { 807 | "ext-dom": "For handling output formats in XML", 808 | "ext-mbstring": "For handling non-UTF8 characters.", 809 | "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", 810 | "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", 811 | "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." 812 | }, 813 | "bin": [ 814 | "php-cs-fixer" 815 | ], 816 | "type": "application", 817 | "autoload": { 818 | "psr-4": { 819 | "PhpCsFixer\\": "src/" 820 | }, 821 | "classmap": [ 822 | "tests/Test/AbstractFixerTestCase.php", 823 | "tests/Test/AbstractIntegrationCaseFactory.php", 824 | "tests/Test/AbstractIntegrationTestCase.php", 825 | "tests/Test/Assert/AssertTokensTrait.php", 826 | "tests/Test/IntegrationCase.php", 827 | "tests/Test/IntegrationCaseFactory.php", 828 | "tests/Test/IntegrationCaseFactoryInterface.php", 829 | "tests/Test/InternalIntegrationCaseFactory.php", 830 | "tests/Test/IsIdenticalConstraint.php", 831 | "tests/TestCase.php" 832 | ] 833 | }, 834 | "notification-url": "https://packagist.org/downloads/", 835 | "license": [ 836 | "MIT" 837 | ], 838 | "authors": [ 839 | { 840 | "name": "Fabien Potencier", 841 | "email": "fabien@symfony.com" 842 | }, 843 | { 844 | "name": "Dariusz Rumiński", 845 | "email": "dariusz.ruminski@gmail.com" 846 | } 847 | ], 848 | "description": "A tool to automatically fix PHP code style", 849 | "time": "2020-06-27T23:57:46+00:00" 850 | }, 851 | { 852 | "name": "kahlan/kahlan", 853 | "version": "4.7.5", 854 | "source": { 855 | "type": "git", 856 | "url": "https://github.com/kahlan/kahlan.git", 857 | "reference": "c529ef24201053ba76d3c8c3531acd76b629ce87" 858 | }, 859 | "dist": { 860 | "type": "zip", 861 | "url": "https://api.github.com/repos/kahlan/kahlan/zipball/c529ef24201053ba76d3c8c3531acd76b629ce87", 862 | "reference": "c529ef24201053ba76d3c8c3531acd76b629ce87", 863 | "shasum": "" 864 | }, 865 | "require": { 866 | "php": ">=5.5" 867 | }, 868 | "require-dev": { 869 | "squizlabs/php_codesniffer": "^3.4" 870 | }, 871 | "bin": [ 872 | "bin/kahlan" 873 | ], 874 | "type": "library", 875 | "autoload": { 876 | "psr-4": { 877 | "Kahlan\\": "src/" 878 | }, 879 | "files": [ 880 | "src/functions.php" 881 | ] 882 | }, 883 | "notification-url": "https://packagist.org/downloads/", 884 | "license": [ 885 | "MIT" 886 | ], 887 | "authors": [ 888 | { 889 | "name": "CrysaLEAD" 890 | } 891 | ], 892 | "description": "The PHP Test Framework for Freedom, Truth and Justice.", 893 | "keywords": [ 894 | "BDD", 895 | "Behavior-Driven Development", 896 | "Monkey Patching", 897 | "TDD", 898 | "mock", 899 | "stub", 900 | "testing", 901 | "unit test" 902 | ], 903 | "time": "2020-04-25T21:27:19+00:00" 904 | }, 905 | { 906 | "name": "paragonie/random_compat", 907 | "version": "v9.99.99", 908 | "source": { 909 | "type": "git", 910 | "url": "https://github.com/paragonie/random_compat.git", 911 | "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" 912 | }, 913 | "dist": { 914 | "type": "zip", 915 | "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", 916 | "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", 917 | "shasum": "" 918 | }, 919 | "require": { 920 | "php": "^7" 921 | }, 922 | "require-dev": { 923 | "phpunit/phpunit": "4.*|5.*", 924 | "vimeo/psalm": "^1" 925 | }, 926 | "suggest": { 927 | "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." 928 | }, 929 | "type": "library", 930 | "notification-url": "https://packagist.org/downloads/", 931 | "license": [ 932 | "MIT" 933 | ], 934 | "authors": [ 935 | { 936 | "name": "Paragon Initiative Enterprises", 937 | "email": "security@paragonie.com", 938 | "homepage": "https://paragonie.com" 939 | } 940 | ], 941 | "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", 942 | "keywords": [ 943 | "csprng", 944 | "polyfill", 945 | "pseudorandom", 946 | "random" 947 | ], 948 | "time": "2018-07-02T15:55:56+00:00" 949 | }, 950 | { 951 | "name": "php-cs-fixer/diff", 952 | "version": "v1.3.0", 953 | "source": { 954 | "type": "git", 955 | "url": "https://github.com/PHP-CS-Fixer/diff.git", 956 | "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756" 957 | }, 958 | "dist": { 959 | "type": "zip", 960 | "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/78bb099e9c16361126c86ce82ec4405ebab8e756", 961 | "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756", 962 | "shasum": "" 963 | }, 964 | "require": { 965 | "php": "^5.6 || ^7.0" 966 | }, 967 | "require-dev": { 968 | "phpunit/phpunit": "^5.7.23 || ^6.4.3", 969 | "symfony/process": "^3.3" 970 | }, 971 | "type": "library", 972 | "autoload": { 973 | "classmap": [ 974 | "src/" 975 | ] 976 | }, 977 | "notification-url": "https://packagist.org/downloads/", 978 | "license": [ 979 | "BSD-3-Clause" 980 | ], 981 | "authors": [ 982 | { 983 | "name": "Kore Nordmann", 984 | "email": "mail@kore-nordmann.de" 985 | }, 986 | { 987 | "name": "Sebastian Bergmann", 988 | "email": "sebastian@phpunit.de" 989 | }, 990 | { 991 | "name": "SpacePossum" 992 | } 993 | ], 994 | "description": "sebastian/diff v2 backport support for PHP5.6", 995 | "homepage": "https://github.com/PHP-CS-Fixer", 996 | "keywords": [ 997 | "diff" 998 | ], 999 | "time": "2018-02-15T16:58:55+00:00" 1000 | }, 1001 | { 1002 | "name": "psr/container", 1003 | "version": "1.0.0", 1004 | "source": { 1005 | "type": "git", 1006 | "url": "https://github.com/php-fig/container.git", 1007 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" 1008 | }, 1009 | "dist": { 1010 | "type": "zip", 1011 | "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 1012 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 1013 | "shasum": "" 1014 | }, 1015 | "require": { 1016 | "php": ">=5.3.0" 1017 | }, 1018 | "type": "library", 1019 | "extra": { 1020 | "branch-alias": { 1021 | "dev-master": "1.0.x-dev" 1022 | } 1023 | }, 1024 | "autoload": { 1025 | "psr-4": { 1026 | "Psr\\Container\\": "src/" 1027 | } 1028 | }, 1029 | "notification-url": "https://packagist.org/downloads/", 1030 | "license": [ 1031 | "MIT" 1032 | ], 1033 | "authors": [ 1034 | { 1035 | "name": "PHP-FIG", 1036 | "homepage": "http://www.php-fig.org/" 1037 | } 1038 | ], 1039 | "description": "Common Container Interface (PHP FIG PSR-11)", 1040 | "homepage": "https://github.com/php-fig/container", 1041 | "keywords": [ 1042 | "PSR-11", 1043 | "container", 1044 | "container-interface", 1045 | "container-interop", 1046 | "psr" 1047 | ], 1048 | "time": "2017-02-14T16:28:37+00:00" 1049 | }, 1050 | { 1051 | "name": "psr/event-dispatcher", 1052 | "version": "1.0.0", 1053 | "source": { 1054 | "type": "git", 1055 | "url": "https://github.com/php-fig/event-dispatcher.git", 1056 | "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" 1057 | }, 1058 | "dist": { 1059 | "type": "zip", 1060 | "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", 1061 | "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", 1062 | "shasum": "" 1063 | }, 1064 | "require": { 1065 | "php": ">=7.2.0" 1066 | }, 1067 | "type": "library", 1068 | "extra": { 1069 | "branch-alias": { 1070 | "dev-master": "1.0.x-dev" 1071 | } 1072 | }, 1073 | "autoload": { 1074 | "psr-4": { 1075 | "Psr\\EventDispatcher\\": "src/" 1076 | } 1077 | }, 1078 | "notification-url": "https://packagist.org/downloads/", 1079 | "license": [ 1080 | "MIT" 1081 | ], 1082 | "authors": [ 1083 | { 1084 | "name": "PHP-FIG", 1085 | "homepage": "http://www.php-fig.org/" 1086 | } 1087 | ], 1088 | "description": "Standard interfaces for event handling.", 1089 | "keywords": [ 1090 | "events", 1091 | "psr", 1092 | "psr-14" 1093 | ], 1094 | "time": "2019-01-08T18:20:26+00:00" 1095 | }, 1096 | { 1097 | "name": "symfony/deprecation-contracts", 1098 | "version": "v2.1.3", 1099 | "source": { 1100 | "type": "git", 1101 | "url": "https://github.com/symfony/deprecation-contracts.git", 1102 | "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14" 1103 | }, 1104 | "dist": { 1105 | "type": "zip", 1106 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5e20b83385a77593259c9f8beb2c43cd03b2ac14", 1107 | "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14", 1108 | "shasum": "" 1109 | }, 1110 | "require": { 1111 | "php": ">=7.1" 1112 | }, 1113 | "type": "library", 1114 | "extra": { 1115 | "branch-alias": { 1116 | "dev-master": "2.1-dev" 1117 | }, 1118 | "thanks": { 1119 | "name": "symfony/contracts", 1120 | "url": "https://github.com/symfony/contracts" 1121 | } 1122 | }, 1123 | "autoload": { 1124 | "files": [ 1125 | "function.php" 1126 | ] 1127 | }, 1128 | "notification-url": "https://packagist.org/downloads/", 1129 | "license": [ 1130 | "MIT" 1131 | ], 1132 | "authors": [ 1133 | { 1134 | "name": "Nicolas Grekas", 1135 | "email": "p@tchwork.com" 1136 | }, 1137 | { 1138 | "name": "Symfony Community", 1139 | "homepage": "https://symfony.com/contributors" 1140 | } 1141 | ], 1142 | "description": "A generic function and convention to trigger deprecation notices", 1143 | "homepage": "https://symfony.com", 1144 | "time": "2020-06-06T08:49:21+00:00" 1145 | }, 1146 | { 1147 | "name": "symfony/event-dispatcher", 1148 | "version": "v5.1.2", 1149 | "source": { 1150 | "type": "git", 1151 | "url": "https://github.com/symfony/event-dispatcher.git", 1152 | "reference": "cc0d059e2e997e79ca34125a52f3e33de4424ac7" 1153 | }, 1154 | "dist": { 1155 | "type": "zip", 1156 | "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/cc0d059e2e997e79ca34125a52f3e33de4424ac7", 1157 | "reference": "cc0d059e2e997e79ca34125a52f3e33de4424ac7", 1158 | "shasum": "" 1159 | }, 1160 | "require": { 1161 | "php": ">=7.2.5", 1162 | "symfony/deprecation-contracts": "^2.1", 1163 | "symfony/event-dispatcher-contracts": "^2", 1164 | "symfony/polyfill-php80": "^1.15" 1165 | }, 1166 | "conflict": { 1167 | "symfony/dependency-injection": "<4.4" 1168 | }, 1169 | "provide": { 1170 | "psr/event-dispatcher-implementation": "1.0", 1171 | "symfony/event-dispatcher-implementation": "2.0" 1172 | }, 1173 | "require-dev": { 1174 | "psr/log": "~1.0", 1175 | "symfony/config": "^4.4|^5.0", 1176 | "symfony/dependency-injection": "^4.4|^5.0", 1177 | "symfony/expression-language": "^4.4|^5.0", 1178 | "symfony/http-foundation": "^4.4|^5.0", 1179 | "symfony/service-contracts": "^1.1|^2", 1180 | "symfony/stopwatch": "^4.4|^5.0" 1181 | }, 1182 | "suggest": { 1183 | "symfony/dependency-injection": "", 1184 | "symfony/http-kernel": "" 1185 | }, 1186 | "type": "library", 1187 | "extra": { 1188 | "branch-alias": { 1189 | "dev-master": "5.1-dev" 1190 | } 1191 | }, 1192 | "autoload": { 1193 | "psr-4": { 1194 | "Symfony\\Component\\EventDispatcher\\": "" 1195 | }, 1196 | "exclude-from-classmap": [ 1197 | "/Tests/" 1198 | ] 1199 | }, 1200 | "notification-url": "https://packagist.org/downloads/", 1201 | "license": [ 1202 | "MIT" 1203 | ], 1204 | "authors": [ 1205 | { 1206 | "name": "Fabien Potencier", 1207 | "email": "fabien@symfony.com" 1208 | }, 1209 | { 1210 | "name": "Symfony Community", 1211 | "homepage": "https://symfony.com/contributors" 1212 | } 1213 | ], 1214 | "description": "Symfony EventDispatcher Component", 1215 | "homepage": "https://symfony.com", 1216 | "time": "2020-05-20T17:43:50+00:00" 1217 | }, 1218 | { 1219 | "name": "symfony/event-dispatcher-contracts", 1220 | "version": "v2.1.3", 1221 | "source": { 1222 | "type": "git", 1223 | "url": "https://github.com/symfony/event-dispatcher-contracts.git", 1224 | "reference": "f6f613d74cfc5a623fc36294d3451eb7fa5a042b" 1225 | }, 1226 | "dist": { 1227 | "type": "zip", 1228 | "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f6f613d74cfc5a623fc36294d3451eb7fa5a042b", 1229 | "reference": "f6f613d74cfc5a623fc36294d3451eb7fa5a042b", 1230 | "shasum": "" 1231 | }, 1232 | "require": { 1233 | "php": ">=7.2.5", 1234 | "psr/event-dispatcher": "^1" 1235 | }, 1236 | "suggest": { 1237 | "symfony/event-dispatcher-implementation": "" 1238 | }, 1239 | "type": "library", 1240 | "extra": { 1241 | "branch-alias": { 1242 | "dev-master": "2.1-dev" 1243 | }, 1244 | "thanks": { 1245 | "name": "symfony/contracts", 1246 | "url": "https://github.com/symfony/contracts" 1247 | } 1248 | }, 1249 | "autoload": { 1250 | "psr-4": { 1251 | "Symfony\\Contracts\\EventDispatcher\\": "" 1252 | } 1253 | }, 1254 | "notification-url": "https://packagist.org/downloads/", 1255 | "license": [ 1256 | "MIT" 1257 | ], 1258 | "authors": [ 1259 | { 1260 | "name": "Nicolas Grekas", 1261 | "email": "p@tchwork.com" 1262 | }, 1263 | { 1264 | "name": "Symfony Community", 1265 | "homepage": "https://symfony.com/contributors" 1266 | } 1267 | ], 1268 | "description": "Generic abstractions related to dispatching event", 1269 | "homepage": "https://symfony.com", 1270 | "keywords": [ 1271 | "abstractions", 1272 | "contracts", 1273 | "decoupling", 1274 | "interfaces", 1275 | "interoperability", 1276 | "standards" 1277 | ], 1278 | "time": "2020-07-06T13:23:11+00:00" 1279 | }, 1280 | { 1281 | "name": "symfony/filesystem", 1282 | "version": "v5.1.2", 1283 | "source": { 1284 | "type": "git", 1285 | "url": "https://github.com/symfony/filesystem.git", 1286 | "reference": "6e4320f06d5f2cce0d96530162491f4465179157" 1287 | }, 1288 | "dist": { 1289 | "type": "zip", 1290 | "url": "https://api.github.com/repos/symfony/filesystem/zipball/6e4320f06d5f2cce0d96530162491f4465179157", 1291 | "reference": "6e4320f06d5f2cce0d96530162491f4465179157", 1292 | "shasum": "" 1293 | }, 1294 | "require": { 1295 | "php": ">=7.2.5", 1296 | "symfony/polyfill-ctype": "~1.8" 1297 | }, 1298 | "type": "library", 1299 | "extra": { 1300 | "branch-alias": { 1301 | "dev-master": "5.1-dev" 1302 | } 1303 | }, 1304 | "autoload": { 1305 | "psr-4": { 1306 | "Symfony\\Component\\Filesystem\\": "" 1307 | }, 1308 | "exclude-from-classmap": [ 1309 | "/Tests/" 1310 | ] 1311 | }, 1312 | "notification-url": "https://packagist.org/downloads/", 1313 | "license": [ 1314 | "MIT" 1315 | ], 1316 | "authors": [ 1317 | { 1318 | "name": "Fabien Potencier", 1319 | "email": "fabien@symfony.com" 1320 | }, 1321 | { 1322 | "name": "Symfony Community", 1323 | "homepage": "https://symfony.com/contributors" 1324 | } 1325 | ], 1326 | "description": "Symfony Filesystem Component", 1327 | "homepage": "https://symfony.com", 1328 | "time": "2020-05-30T20:35:19+00:00" 1329 | }, 1330 | { 1331 | "name": "symfony/finder", 1332 | "version": "v5.1.2", 1333 | "source": { 1334 | "type": "git", 1335 | "url": "https://github.com/symfony/finder.git", 1336 | "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187" 1337 | }, 1338 | "dist": { 1339 | "type": "zip", 1340 | "url": "https://api.github.com/repos/symfony/finder/zipball/4298870062bfc667cb78d2b379be4bf5dec5f187", 1341 | "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187", 1342 | "shasum": "" 1343 | }, 1344 | "require": { 1345 | "php": ">=7.2.5" 1346 | }, 1347 | "type": "library", 1348 | "extra": { 1349 | "branch-alias": { 1350 | "dev-master": "5.1-dev" 1351 | } 1352 | }, 1353 | "autoload": { 1354 | "psr-4": { 1355 | "Symfony\\Component\\Finder\\": "" 1356 | }, 1357 | "exclude-from-classmap": [ 1358 | "/Tests/" 1359 | ] 1360 | }, 1361 | "notification-url": "https://packagist.org/downloads/", 1362 | "license": [ 1363 | "MIT" 1364 | ], 1365 | "authors": [ 1366 | { 1367 | "name": "Fabien Potencier", 1368 | "email": "fabien@symfony.com" 1369 | }, 1370 | { 1371 | "name": "Symfony Community", 1372 | "homepage": "https://symfony.com/contributors" 1373 | } 1374 | ], 1375 | "description": "Symfony Finder Component", 1376 | "homepage": "https://symfony.com", 1377 | "time": "2020-05-20T17:43:50+00:00" 1378 | }, 1379 | { 1380 | "name": "symfony/options-resolver", 1381 | "version": "v5.1.2", 1382 | "source": { 1383 | "type": "git", 1384 | "url": "https://github.com/symfony/options-resolver.git", 1385 | "reference": "663f5dd5e14057d1954fe721f9709d35837f2447" 1386 | }, 1387 | "dist": { 1388 | "type": "zip", 1389 | "url": "https://api.github.com/repos/symfony/options-resolver/zipball/663f5dd5e14057d1954fe721f9709d35837f2447", 1390 | "reference": "663f5dd5e14057d1954fe721f9709d35837f2447", 1391 | "shasum": "" 1392 | }, 1393 | "require": { 1394 | "php": ">=7.2.5", 1395 | "symfony/deprecation-contracts": "^2.1", 1396 | "symfony/polyfill-php80": "^1.15" 1397 | }, 1398 | "type": "library", 1399 | "extra": { 1400 | "branch-alias": { 1401 | "dev-master": "5.1-dev" 1402 | } 1403 | }, 1404 | "autoload": { 1405 | "psr-4": { 1406 | "Symfony\\Component\\OptionsResolver\\": "" 1407 | }, 1408 | "exclude-from-classmap": [ 1409 | "/Tests/" 1410 | ] 1411 | }, 1412 | "notification-url": "https://packagist.org/downloads/", 1413 | "license": [ 1414 | "MIT" 1415 | ], 1416 | "authors": [ 1417 | { 1418 | "name": "Fabien Potencier", 1419 | "email": "fabien@symfony.com" 1420 | }, 1421 | { 1422 | "name": "Symfony Community", 1423 | "homepage": "https://symfony.com/contributors" 1424 | } 1425 | ], 1426 | "description": "Symfony OptionsResolver Component", 1427 | "homepage": "https://symfony.com", 1428 | "keywords": [ 1429 | "config", 1430 | "configuration", 1431 | "options" 1432 | ], 1433 | "time": "2020-05-23T13:08:13+00:00" 1434 | }, 1435 | { 1436 | "name": "symfony/polyfill-php70", 1437 | "version": "v1.18.0", 1438 | "source": { 1439 | "type": "git", 1440 | "url": "https://github.com/symfony/polyfill-php70.git", 1441 | "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3" 1442 | }, 1443 | "dist": { 1444 | "type": "zip", 1445 | "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", 1446 | "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", 1447 | "shasum": "" 1448 | }, 1449 | "require": { 1450 | "paragonie/random_compat": "~1.0|~2.0|~9.99", 1451 | "php": ">=5.3.3" 1452 | }, 1453 | "type": "library", 1454 | "extra": { 1455 | "branch-alias": { 1456 | "dev-master": "1.18-dev" 1457 | }, 1458 | "thanks": { 1459 | "name": "symfony/polyfill", 1460 | "url": "https://github.com/symfony/polyfill" 1461 | } 1462 | }, 1463 | "autoload": { 1464 | "psr-4": { 1465 | "Symfony\\Polyfill\\Php70\\": "" 1466 | }, 1467 | "files": [ 1468 | "bootstrap.php" 1469 | ], 1470 | "classmap": [ 1471 | "Resources/stubs" 1472 | ] 1473 | }, 1474 | "notification-url": "https://packagist.org/downloads/", 1475 | "license": [ 1476 | "MIT" 1477 | ], 1478 | "authors": [ 1479 | { 1480 | "name": "Nicolas Grekas", 1481 | "email": "p@tchwork.com" 1482 | }, 1483 | { 1484 | "name": "Symfony Community", 1485 | "homepage": "https://symfony.com/contributors" 1486 | } 1487 | ], 1488 | "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", 1489 | "homepage": "https://symfony.com", 1490 | "keywords": [ 1491 | "compatibility", 1492 | "polyfill", 1493 | "portable", 1494 | "shim" 1495 | ], 1496 | "time": "2020-07-14T12:35:20+00:00" 1497 | }, 1498 | { 1499 | "name": "symfony/polyfill-php72", 1500 | "version": "v1.18.0", 1501 | "source": { 1502 | "type": "git", 1503 | "url": "https://github.com/symfony/polyfill-php72.git", 1504 | "reference": "639447d008615574653fb3bc60d1986d7172eaae" 1505 | }, 1506 | "dist": { 1507 | "type": "zip", 1508 | "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", 1509 | "reference": "639447d008615574653fb3bc60d1986d7172eaae", 1510 | "shasum": "" 1511 | }, 1512 | "require": { 1513 | "php": ">=5.3.3" 1514 | }, 1515 | "type": "library", 1516 | "extra": { 1517 | "branch-alias": { 1518 | "dev-master": "1.18-dev" 1519 | }, 1520 | "thanks": { 1521 | "name": "symfony/polyfill", 1522 | "url": "https://github.com/symfony/polyfill" 1523 | } 1524 | }, 1525 | "autoload": { 1526 | "psr-4": { 1527 | "Symfony\\Polyfill\\Php72\\": "" 1528 | }, 1529 | "files": [ 1530 | "bootstrap.php" 1531 | ] 1532 | }, 1533 | "notification-url": "https://packagist.org/downloads/", 1534 | "license": [ 1535 | "MIT" 1536 | ], 1537 | "authors": [ 1538 | { 1539 | "name": "Nicolas Grekas", 1540 | "email": "p@tchwork.com" 1541 | }, 1542 | { 1543 | "name": "Symfony Community", 1544 | "homepage": "https://symfony.com/contributors" 1545 | } 1546 | ], 1547 | "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", 1548 | "homepage": "https://symfony.com", 1549 | "keywords": [ 1550 | "compatibility", 1551 | "polyfill", 1552 | "portable", 1553 | "shim" 1554 | ], 1555 | "time": "2020-07-14T12:35:20+00:00" 1556 | }, 1557 | { 1558 | "name": "symfony/process", 1559 | "version": "v5.1.2", 1560 | "source": { 1561 | "type": "git", 1562 | "url": "https://github.com/symfony/process.git", 1563 | "reference": "7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1" 1564 | }, 1565 | "dist": { 1566 | "type": "zip", 1567 | "url": "https://api.github.com/repos/symfony/process/zipball/7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1", 1568 | "reference": "7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1", 1569 | "shasum": "" 1570 | }, 1571 | "require": { 1572 | "php": ">=7.2.5", 1573 | "symfony/polyfill-php80": "^1.15" 1574 | }, 1575 | "type": "library", 1576 | "extra": { 1577 | "branch-alias": { 1578 | "dev-master": "5.1-dev" 1579 | } 1580 | }, 1581 | "autoload": { 1582 | "psr-4": { 1583 | "Symfony\\Component\\Process\\": "" 1584 | }, 1585 | "exclude-from-classmap": [ 1586 | "/Tests/" 1587 | ] 1588 | }, 1589 | "notification-url": "https://packagist.org/downloads/", 1590 | "license": [ 1591 | "MIT" 1592 | ], 1593 | "authors": [ 1594 | { 1595 | "name": "Fabien Potencier", 1596 | "email": "fabien@symfony.com" 1597 | }, 1598 | { 1599 | "name": "Symfony Community", 1600 | "homepage": "https://symfony.com/contributors" 1601 | } 1602 | ], 1603 | "description": "Symfony Process Component", 1604 | "homepage": "https://symfony.com", 1605 | "time": "2020-05-30T20:35:19+00:00" 1606 | }, 1607 | { 1608 | "name": "symfony/service-contracts", 1609 | "version": "v2.1.3", 1610 | "source": { 1611 | "type": "git", 1612 | "url": "https://github.com/symfony/service-contracts.git", 1613 | "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442" 1614 | }, 1615 | "dist": { 1616 | "type": "zip", 1617 | "url": "https://api.github.com/repos/symfony/service-contracts/zipball/58c7475e5457c5492c26cc740cc0ad7464be9442", 1618 | "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442", 1619 | "shasum": "" 1620 | }, 1621 | "require": { 1622 | "php": ">=7.2.5", 1623 | "psr/container": "^1.0" 1624 | }, 1625 | "suggest": { 1626 | "symfony/service-implementation": "" 1627 | }, 1628 | "type": "library", 1629 | "extra": { 1630 | "branch-alias": { 1631 | "dev-master": "2.1-dev" 1632 | }, 1633 | "thanks": { 1634 | "name": "symfony/contracts", 1635 | "url": "https://github.com/symfony/contracts" 1636 | } 1637 | }, 1638 | "autoload": { 1639 | "psr-4": { 1640 | "Symfony\\Contracts\\Service\\": "" 1641 | } 1642 | }, 1643 | "notification-url": "https://packagist.org/downloads/", 1644 | "license": [ 1645 | "MIT" 1646 | ], 1647 | "authors": [ 1648 | { 1649 | "name": "Nicolas Grekas", 1650 | "email": "p@tchwork.com" 1651 | }, 1652 | { 1653 | "name": "Symfony Community", 1654 | "homepage": "https://symfony.com/contributors" 1655 | } 1656 | ], 1657 | "description": "Generic abstractions related to writing services", 1658 | "homepage": "https://symfony.com", 1659 | "keywords": [ 1660 | "abstractions", 1661 | "contracts", 1662 | "decoupling", 1663 | "interfaces", 1664 | "interoperability", 1665 | "standards" 1666 | ], 1667 | "time": "2020-07-06T13:23:11+00:00" 1668 | }, 1669 | { 1670 | "name": "symfony/stopwatch", 1671 | "version": "v5.1.2", 1672 | "source": { 1673 | "type": "git", 1674 | "url": "https://github.com/symfony/stopwatch.git", 1675 | "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323" 1676 | }, 1677 | "dist": { 1678 | "type": "zip", 1679 | "url": "https://api.github.com/repos/symfony/stopwatch/zipball/0f7c58cf81dbb5dd67d423a89d577524a2ec0323", 1680 | "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323", 1681 | "shasum": "" 1682 | }, 1683 | "require": { 1684 | "php": ">=7.2.5", 1685 | "symfony/service-contracts": "^1.0|^2" 1686 | }, 1687 | "type": "library", 1688 | "extra": { 1689 | "branch-alias": { 1690 | "dev-master": "5.1-dev" 1691 | } 1692 | }, 1693 | "autoload": { 1694 | "psr-4": { 1695 | "Symfony\\Component\\Stopwatch\\": "" 1696 | }, 1697 | "exclude-from-classmap": [ 1698 | "/Tests/" 1699 | ] 1700 | }, 1701 | "notification-url": "https://packagist.org/downloads/", 1702 | "license": [ 1703 | "MIT" 1704 | ], 1705 | "authors": [ 1706 | { 1707 | "name": "Fabien Potencier", 1708 | "email": "fabien@symfony.com" 1709 | }, 1710 | { 1711 | "name": "Symfony Community", 1712 | "homepage": "https://symfony.com/contributors" 1713 | } 1714 | ], 1715 | "description": "Symfony Stopwatch Component", 1716 | "homepage": "https://symfony.com", 1717 | "time": "2020-05-20T17:43:50+00:00" 1718 | } 1719 | ], 1720 | "aliases": [], 1721 | "minimum-stability": "stable", 1722 | "stability-flags": [], 1723 | "prefer-stable": false, 1724 | "prefer-lowest": false, 1725 | "platform": { 1726 | "php": "^7.2" 1727 | }, 1728 | "platform-dev": [] 1729 | } 1730 | -------------------------------------------------------------------------------- /resources/dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmsipilot/docker-compose-viz/b4e540909266492ac2d3c0ae3d7391c789b5e4c8/resources/dependencies.png -------------------------------------------------------------------------------- /resources/display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmsipilot/docker-compose-viz/b4e540909266492ac2d3c0ae3d7391c789b5e4c8/resources/display.png -------------------------------------------------------------------------------- /resources/extends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmsipilot/docker-compose-viz/b4e540909266492ac2d3c0ae3d7391c789b5e4c8/resources/extends.png -------------------------------------------------------------------------------- /resources/external_links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmsipilot/docker-compose-viz/b4e540909266492ac2d3c0ae3d7391c789b5e4c8/resources/external_links.png -------------------------------------------------------------------------------- /resources/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmsipilot/docker-compose-viz/b4e540909266492ac2d3c0ae3d7391c789b5e4c8/resources/image.png -------------------------------------------------------------------------------- /resources/links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmsipilot/docker-compose-viz/b4e540909266492ac2d3c0ae3d7391c789b5e4c8/resources/links.png -------------------------------------------------------------------------------- /resources/networks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmsipilot/docker-compose-viz/b4e540909266492ac2d3c0ae3d7391c789b5e4c8/resources/networks.png -------------------------------------------------------------------------------- /resources/ports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmsipilot/docker-compose-viz/b4e540909266492ac2d3c0ae3d7391c789b5e4c8/resources/ports.png -------------------------------------------------------------------------------- /resources/volumes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmsipilot/docker-compose-viz/b4e540909266492ac2d3c0ae3d7391c789b5e4c8/resources/volumes.png -------------------------------------------------------------------------------- /spec/fetch-networks.php: -------------------------------------------------------------------------------- 1 | ['image' => 'bar']]; 11 | 12 | expect(fetchNetworks($configuration))->toBe([]); 13 | }); 14 | }); 15 | 16 | describe('from a version 2 configuration', function () { 17 | it('should fetch networks from the dedicated section', function () { 18 | $configuration = ['version' => 2, 'networks' => ['foo' => [], 'bar' => []]]; 19 | 20 | expect(fetchNetworks($configuration))->toBe($configuration['networks']); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /spec/fetch-services.php: -------------------------------------------------------------------------------- 1 | ['image' => 'bar'], 'baz' => ['build' => '.']]; 11 | 12 | expect(fetchServices($configuration))->toBe($configuration); 13 | }); 14 | }); 15 | 16 | describe('from a version 2 configuration', function () { 17 | it('should fetch services from the dedicated section', function () { 18 | $configuration = ['version' => 2, 'services' => ['foo' => ['image' => 'bar'], 'baz' => ['build' => '.']]]; 19 | 20 | expect(fetchServices($configuration))->toBe($configuration['services']); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /spec/fetch-volumes.php: -------------------------------------------------------------------------------- 1 | ['image' => 'bar']]; 11 | 12 | expect(fetchVolumes($configuration))->toBe([]); 13 | }); 14 | }); 15 | 16 | describe('from a version 2 configuration', function () { 17 | it('should fetch volumes from the dedicated section', function () { 18 | $configuration = ['version' => 2, 'volumes' => ['foo' => [], 'bar' => []]]; 19 | 20 | expect(fetchVolumes($configuration))->toBe($configuration['volumes']); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /spec/fixtures/read-configuration/invalid.yml: -------------------------------------------------------------------------------- 1 | version 2 | services: 3 | foo: 4 | image: bar 5 | -------------------------------------------------------------------------------- /spec/fixtures/read-configuration/valid.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | services: 3 | foo: 4 | image: bar 5 | -------------------------------------------------------------------------------- /spec/read-configuratoin.php: -------------------------------------------------------------------------------- 1 | toThrow(new InvalidArgumentException()); 13 | }); 14 | 15 | it('should parse YAML and return an array', function () { 16 | expect(readConfiguration(__DIR__.'/fixtures/read-configuration/valid.yml')) 17 | ->toBe(['version' => 2, 'services' => ['foo' => ['image' => 'bar']]]); 18 | }); 19 | 20 | it('should report if YAML is invalid', function () { 21 | expect(function () { 22 | readConfiguration(__DIR__.'/fixtures/read-configuration/invalid.yml'); 23 | }) 24 | ->toThrow(new InvalidArgumentException()); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/application.php: -------------------------------------------------------------------------------- 1 | register('render') 11 | ->addArgument('input-file', Console\Input\InputArgument::OPTIONAL, 'Path to a docker compose file', getcwd().DIRECTORY_SEPARATOR.'docker-compose.yml') 12 | 13 | ->addOption('override', null, Console\Input\InputOption::VALUE_REQUIRED, 'Tag of the override file to use', 'override') 14 | ->addOption('output-file', 'o', Console\Input\InputOption::VALUE_REQUIRED, 'Path to a output file (Only for "dot" and "image" output format)') 15 | ->addOption('output-format', 'm', Console\Input\InputOption::VALUE_REQUIRED, 'Output format (one of: "dot", "image", "display", "graphviz")', 'display') 16 | ->addOption('graphviz-output-format', null, Console\Input\InputOption::VALUE_REQUIRED, 'GraphViz Output format (see `man dot` for details)', 'svg') 17 | ->addOption('only', null, Console\Input\InputOption::VALUE_IS_ARRAY | Console\Input\InputOption::VALUE_REQUIRED, 'Display a graph only for a given services') 18 | 19 | ->addOption('force', 'f', Console\Input\InputOption::VALUE_NONE, 'Overwrites output file if it already exists') 20 | ->addOption('no-volumes', null, Console\Input\InputOption::VALUE_NONE, 'Do not display volumes') 21 | ->addOption('no-networks', null, Console\Input\InputOption::VALUE_NONE, 'Do not display networks') 22 | ->addOption('no-ports', null, Console\Input\InputOption::VALUE_NONE, 'Do not display ports') 23 | ->addOption('horizontal', 'r', Console\Input\InputOption::VALUE_NONE, 'Display a horizontal graph') 24 | ->addOption('ignore-override', null, Console\Input\InputOption::VALUE_NONE, 'Ignore override file') 25 | ->addOption('background', null, Console\Input\InputOption::VALUE_REQUIRED, 'Set the graph background color', '#ffffff') 26 | 27 | ->setCode(function (Console\Input\InputInterface $input, Console\Output\OutputInterface $output) { 28 | $backgroundColor = $input->getOption('background'); 29 | 30 | if (0 === preg_match('/^#[a-fA-F0-9]{6}|transparent$/', $backgroundColor)) { 31 | throw new Console\Exception\InvalidArgumentException(sprintf('Invalid background color "%s". It must be a valid hex color or "transparent".', $backgroundColor)); 32 | } 33 | 34 | $logger = logger($output); 35 | $inputFile = $input->getArgument('input-file'); 36 | $inputFileExtension = pathinfo($inputFile, PATHINFO_EXTENSION); 37 | $overrideFile = dirname($inputFile).DIRECTORY_SEPARATOR.basename($inputFile, '.'.$inputFileExtension).'.'.$input->getOption('override').'.'.$inputFileExtension; 38 | 39 | $outputFormat = $input->getOption('output-format'); 40 | $outputFile = $input->getOption('output-file') ?: getcwd().DIRECTORY_SEPARATOR.'docker-compose.'.('dot' === $outputFormat ? $outputFormat : 'png'); 41 | $onlyServices = $input->getOption('only'); 42 | 43 | if (false === in_array($outputFormat, ['dot', 'image', 'display', 'graphviz'])) { 44 | throw new Console\Exception\InvalidArgumentException(sprintf('Invalid output format "%s". It must be one of "dot", "image" or "display".', $outputFormat)); 45 | } 46 | 47 | if ('display' === $outputFormat) { 48 | if ($input->getOption('force') || $input->getOption('output-file')) { 49 | $output->writeln('The following options are ignored with the "display" output format: "--force", "--output-file"'); 50 | } 51 | } else { 52 | if (true === file_exists($outputFile) && false === $input->getOption('force')) { 53 | throw new Console\Exception\InvalidArgumentException(sprintf('File "%s" already exists. Use the "--force" option to overwrite it.', $outputFile)); 54 | } 55 | } 56 | 57 | $logger(sprintf('Reading configuration from "%s"', $inputFile)); 58 | $configuration = readConfiguration($inputFile); 59 | $configurationVersion = (string) ($configuration['version'] ?? 1); 60 | 61 | if (!$input->getOption('ignore-override') && file_exists($overrideFile)) { 62 | $logger(sprintf('Reading override from "%s"', $overrideFile)); 63 | $override = readConfiguration($overrideFile); 64 | $overrideVersion = (string) ($override['version'] ?? 1); 65 | 66 | if ($configurationVersion !== $overrideVersion) { 67 | throw new Console\Exception\LogicException(sprintf('Version mismatch: file "%s" specifies version "%s" but file "%s" uses version "%s"', $inputFile, $configurationVersion, $overrideFile, $overrideVersion)); 68 | } 69 | 70 | $configuration = array_merge_recursive($configuration, $override); 71 | 72 | $logger(sprintf('Configuration version is "%s"', $configurationVersion), Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE); 73 | $configuration['version'] = $configurationVersion; 74 | } 75 | 76 | $logger('Fetching services'); 77 | $services = fetchServices($configuration); 78 | $logger(sprintf('Found %d services', count($services)), Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE); 79 | 80 | $logger('Fetching volumes'); 81 | $volumes = fetchVolumes($configuration); 82 | $logger(sprintf('Found %d volumes', count($volumes)), Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE); 83 | 84 | $logger('Fetching networks'); 85 | $networks = fetchNetworks($configuration); 86 | $logger(sprintf('Found %d networks', count($networks)), Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE); 87 | 88 | if ([] !== $onlyServices) { 89 | $logger(sprintf('Only %s services will be displayed', implode(', ', $onlyServices))); 90 | 91 | $intersect = array_intersect($onlyServices, array_keys($services)); 92 | 93 | if ($intersect !== $onlyServices) { 94 | throw new Console\Exception\InvalidArgumentException(sprintf('The following services do not exist: "%s"', implode('", "', array_diff($onlyServices, $intersect)))); 95 | } 96 | 97 | $services = array_filter( 98 | $services, 99 | function ($service) use ($onlyServices) { 100 | return in_array($service, $onlyServices); 101 | }, 102 | ARRAY_FILTER_USE_KEY 103 | ); 104 | } 105 | 106 | $flags = 0; 107 | if (true === $input->getOption('no-volumes')) { 108 | $logger('Volumes will not be displayed'); 109 | 110 | $flags |= WITHOUT_VOLUMES; 111 | } 112 | 113 | if (true === $input->getOption('no-networks')) { 114 | $logger('Networks will not be displayed'); 115 | 116 | $flags |= WITHOUT_NETWORKS; 117 | } 118 | 119 | if (true === $input->getOption('no-ports')) { 120 | $logger('Ports will not be displayed'); 121 | 122 | $flags |= WITHOUT_PORTS; 123 | } 124 | 125 | $logger('Rendering graph'); 126 | $graph = applyGraphvizStyle( 127 | createGraph($services, $volumes, $networks, $inputFile, $flags), 128 | $input->getOption('horizontal'), 129 | $input->getOption('background') 130 | ); 131 | 132 | switch ($outputFormat) { 133 | case 'dot': 134 | case 'image': 135 | $rendererClass = 'Graphp\GraphViz\\'.ucfirst($outputFormat); 136 | $renderer = new $rendererClass(); 137 | 138 | file_put_contents($outputFile, $renderer->getOutput($graph)); 139 | break; 140 | 141 | case 'display': 142 | $renderer = new GraphViz(); 143 | $renderer->display($graph); 144 | break; 145 | 146 | case 'graphviz': 147 | $renderer = new GraphViz(); 148 | $format = $input->getOption('graphviz-output-format'); 149 | 150 | file_put_contents($outputFile, $renderer->setFormat($format)->createImageData($graph)); 151 | break; 152 | } 153 | }); 154 | 155 | $application->run(); 156 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | writeln(sprintf('[%s] %s', date(DATE_ISO8601), $message), $verbosity ?: OutputInterface::VERBOSITY_VERBOSE); 24 | }; 25 | } 26 | 27 | /** 28 | * @public 29 | * 30 | * @param string $path Path to a YAML file 31 | */ 32 | function readConfiguration(string $path): array 33 | { 34 | if (false === file_exists($path)) { 35 | throw new InvalidArgumentException(sprintf('File "%s" does not exist', $path)); 36 | } 37 | 38 | try { 39 | return Yaml::parse(file_get_contents($path)); 40 | } catch (ParseException $exception) { 41 | throw new InvalidArgumentException(sprintf('File "%s" does not contain valid YAML', $path), $exception->getCode(), $exception); 42 | } 43 | } 44 | 45 | /** 46 | * @public 47 | * 48 | * @param array $configuration Docker compose (version 1 or 2) configuration 49 | * 50 | * @return array List of service definitions exctracted from the configuration 51 | */ 52 | function fetchServices(array $configuration): array 53 | { 54 | if (false === isset($configuration['version']) || 1 === (int) $configuration['version']) { 55 | return $configuration; 56 | } 57 | 58 | return $configuration['services'] ?? []; 59 | } 60 | 61 | /** 62 | * @public 63 | * 64 | * @param array $configuration Docker compose (version 1 or 2) configuration 65 | * 66 | * @return array List of service definitions exctracted from the configuration 67 | */ 68 | function fetchVolumes(array $configuration): array 69 | { 70 | if (false === isset($configuration['version']) || 1 === (int) $configuration['version']) { 71 | return []; 72 | } 73 | 74 | return $configuration['volumes'] ?? []; 75 | } 76 | 77 | /** 78 | * @public 79 | * 80 | * @param array $configuration Docker compose (version 1 or 2) configuration 81 | * 82 | * @return array List of service definitions exctracted from the configuration 83 | */ 84 | function fetchNetworks(array $configuration): array 85 | { 86 | if (false === isset($configuration['version']) || 1 === (int) $configuration['version']) { 87 | return []; 88 | } 89 | 90 | return $configuration['networks'] ?? []; 91 | } 92 | 93 | /** 94 | * @public 95 | * 96 | * @param array $services Docker compose service definitions 97 | * @param array $volumes Docker compose volume definitions 98 | * @param array $networks Docker compose network definitions 99 | * @param bool $withVolumes Create vertices and edges for volumes 100 | * @param string $path Path of the current docker-compose configuration file 101 | * 102 | * @return Graph The complete graph for the given list of services 103 | */ 104 | function createGraph(array $services, array $volumes, array $networks, string $path, int $flags): Graph 105 | { 106 | return makeVerticesAndEdges(new Graph(), $services, $volumes, $networks, $path, $flags); 107 | } 108 | 109 | /** 110 | * @public 111 | * 112 | * @param Graph $graph Input graph 113 | * @param bool $horizontal Display a horizontal graph 114 | * @param string $horizontal Background color (any hex color or 'transparent') 115 | * 116 | * @return Graph A copy of the input graph with style attributes 117 | */ 118 | function applyGraphvizStyle(Graph $graph, bool $horizontal, string $background): Graph 119 | { 120 | $graph = $graph->createGraphClone(); 121 | $graph->setAttribute('graphviz.graph.bgcolor', $background); 122 | $graph->setAttribute('graphviz.graph.pad', '0.5'); 123 | $graph->setAttribute('graphviz.graph.ratio', 'fill'); 124 | 125 | if (true === $horizontal) { 126 | $graph->setAttribute('graphviz.graph.rankdir', 'LR'); 127 | } 128 | 129 | foreach ($graph->getVertices() as $vertex) { 130 | switch ($vertex->getAttribute('docker_compose.type')) { 131 | case 'service': 132 | $vertex->setAttribute('graphviz.shape', 'component'); 133 | break; 134 | 135 | case 'external_service': 136 | $vertex->setAttribute('graphviz.shape', 'component'); 137 | $vertex->setAttribute('graphviz.color', 'gray'); 138 | break; 139 | 140 | case 'volume': 141 | $vertex->setAttribute('graphviz.shape', 'folder'); 142 | break; 143 | 144 | case 'network': 145 | $vertex->setAttribute('graphviz.shape', 'pentagon'); 146 | break; 147 | 148 | case 'external_network': 149 | $vertex->setAttribute('graphviz.shape', 'pentagon'); 150 | $vertex->setAttribute('graphviz.color', 'gray'); 151 | break; 152 | 153 | case 'port': 154 | $vertex->setAttribute('graphviz.shape', 'circle'); 155 | 156 | if ('udp' === ($proto = $vertex->getAttribute('docker_compose.proto'))) { 157 | $vertex->setAttribute('graphviz.style', 'dashed'); 158 | } 159 | break; 160 | } 161 | } 162 | 163 | foreach ($graph->getEdges() as $edge) { 164 | switch ($edge->getAttribute('docker_compose.type')) { 165 | case 'ports': 166 | case 'links': 167 | $edge->setAttribute('graphviz.style', 'solid'); 168 | break; 169 | 170 | case 'external_links': 171 | $edge->setAttribute('graphviz.style', 'solid'); 172 | $edge->setAttribute('graphviz.color', 'gray'); 173 | break; 174 | 175 | case 'volumes_from': 176 | case 'volumes': 177 | $edge->setAttribute('graphviz.style', 'dashed'); 178 | break; 179 | 180 | case 'depends_on': 181 | $edge->setAttribute('graphviz.style', 'dotted'); 182 | break; 183 | 184 | case 'extends': 185 | $edge->setAttribute('graphviz.dir', 'both'); 186 | $edge->setAttribute('graphviz.arrowhead', 'inv'); 187 | $edge->setAttribute('graphviz.arrowtail', 'dot'); 188 | break; 189 | } 190 | 191 | if (null !== ($alias = $edge->getAttribute('docker_compose.alias'))) { 192 | $edge->setAttribute('graphviz.label', $alias); 193 | 194 | if (null !== $edge->getAttribute('docker_compose.condition')) { 195 | $edge->setAttribute('graphviz.fontsize', '10'); 196 | } 197 | } 198 | 199 | if ($edge->getAttribute('docker_compose.bidir')) { 200 | $edge->setAttribute('graphviz.dir', 'both'); 201 | } 202 | } 203 | 204 | return $graph; 205 | } 206 | 207 | /** 208 | * @internal 209 | * 210 | * @param Graph $graph Input graph 211 | * @param array $services Docker compose service definitions 212 | * @param array $volumes Docker compose volume definitions 213 | * @param array $networks Docker compose network definitions 214 | * @param bool $withVolumes Create vertices and edges for volumes 215 | * 216 | * @return Graph A copy of the input graph with vertices and edges for services 217 | */ 218 | function makeVerticesAndEdges(Graph $graph, array $services, array $volumes, array $networks, string $path, int $flags): Graph 219 | { 220 | if (false === ((bool) ($flags & WITHOUT_VOLUMES))) { 221 | foreach (array_keys($volumes) as $volume) { 222 | addVolume($graph, 'named: '.$volume); 223 | } 224 | } 225 | 226 | if (false === ((bool) ($flags & WITHOUT_NETWORKS))) { 227 | foreach ($networks as $network => $definition) { 228 | addNetwork( 229 | $graph, 230 | 'net: '.$network, 231 | isset($definition['external']) && true === $definition['external'] ? 'external_network' : 'network' 232 | ); 233 | } 234 | } 235 | 236 | foreach ($services as $service => $definition) { 237 | addService($graph, $service); 238 | 239 | if (isset($definition['extends'])) { 240 | if (isset($definition['extends']['file'])) { 241 | $configuration = readConfiguration(dirname($path).DIRECTORY_SEPARATOR.$definition['extends']['file']); 242 | $extendedServices = fetchServices($configuration); 243 | $extendedVolumes = fetchVolumes($configuration); 244 | $extendedNetworks = fetchNetworks($configuration); 245 | 246 | $graph = makeVerticesAndEdges($graph, $extendedServices, $extendedVolumes, $extendedNetworks, dirname($path).DIRECTORY_SEPARATOR.$definition['extends']['file'], $flags); 247 | } 248 | 249 | addRelation( 250 | addService($graph, $definition['extends']['service']), 251 | $graph->getVertex($service), 252 | 'extends' 253 | ); 254 | } 255 | 256 | $serviceLinks = []; 257 | 258 | foreach ($definition['links'] ?? [] as $link) { 259 | list($target, $alias) = explodeMapping($link); 260 | 261 | $serviceLinks[$alias] = $target; 262 | } 263 | 264 | foreach ($serviceLinks as $alias => $target) { 265 | addRelation( 266 | addService($graph, $target), 267 | $graph->getVertex($service), 268 | 'links', 269 | $alias !== $target ? $alias : null 270 | ); 271 | } 272 | 273 | foreach ($definition['external_links'] ?? [] as $link) { 274 | list($target, $alias) = explodeMapping($link); 275 | 276 | addRelation( 277 | addService($graph, $target, 'external_service'), 278 | $graph->getVertex($service), 279 | 'external_links', 280 | $alias !== $target ? $alias : null 281 | ); 282 | } 283 | 284 | foreach ($definition['depends_on'] ?? [] as $key => $dependency) { 285 | addRelation( 286 | $graph->getVertex($service), 287 | addService($graph, is_array($dependency) ? $key : $dependency), 288 | 'depends_on', 289 | is_array($dependency) && isset($dependency['condition']) ? $dependency['condition'] : null, 290 | false, 291 | is_array($dependency) && isset($dependency['condition']) 292 | ); 293 | } 294 | 295 | foreach ($definition['volumes_from'] ?? [] as $source) { 296 | addRelation( 297 | addService($graph, $source), 298 | $graph->getVertex($service), 299 | 'volumes_from' 300 | ); 301 | } 302 | 303 | if (false === ((bool) ($flags & WITHOUT_VOLUMES))) { 304 | $serviceVolumes = []; 305 | 306 | foreach ($definition['volumes'] ?? [] as $volume) { 307 | if (is_array($volume)) { 308 | $host = $volume['source']; 309 | $container = $volume['target']; 310 | $attr = !empty($volume['read-only']) ? 'ro' : ''; 311 | } else { 312 | list($host, $container, $attr) = explodeVolumeMapping($volume); 313 | } 314 | 315 | $serviceVolumes[$container] = [$host, $attr]; 316 | } 317 | 318 | foreach ($serviceVolumes as $container => $volume) { 319 | list($host, $attr) = $volume; 320 | 321 | if ('.' !== $host[0] && DIRECTORY_SEPARATOR !== $host[0]) { 322 | $host = 'named: '.$host; 323 | } 324 | 325 | addRelation( 326 | addVolume($graph, $host), 327 | $graph->getVertex($service), 328 | 'volumes', 329 | $host !== $container ? $container : null, 330 | 'ro' !== $attr 331 | ); 332 | } 333 | } 334 | 335 | if (false === ((bool) ($flags & WITHOUT_PORTS))) { 336 | foreach ($definition['ports'] ?? [] as $port) { 337 | list($target, $host, $container, $proto) = explodePortMapping($port); 338 | 339 | addRelation( 340 | addPort($graph, (int) $host, $proto, $target), 341 | $graph->getVertex($service), 342 | 'ports', 343 | $host !== $container ? $container : null 344 | ); 345 | } 346 | } 347 | 348 | if (false === ((bool) ($flags & WITHOUT_NETWORKS))) { 349 | foreach ($definition['networks'] ?? [] as $network => $config) { 350 | $network = is_int($network) ? $config : $network; 351 | $config = is_int($network) ? [] : $config; 352 | $aliases = $config['aliases'] ?? []; 353 | 354 | addRelation( 355 | $graph->getVertex($service), 356 | addNetwork($graph, 'net: '.$network), 357 | 'networks', 358 | count($aliases) > 0 ? implode(', ', $aliases) : null 359 | ); 360 | } 361 | } 362 | } 363 | 364 | return $graph; 365 | } 366 | 367 | /** 368 | * @internal 369 | * 370 | * @param Graph $graph Input graph 371 | * @param string $service Service name 372 | * @param string $type Service type 373 | * 374 | * @return Vertex 375 | */ 376 | function addService(Graph $graph, string $service, string $type = null) 377 | { 378 | if (true === $graph->hasVertex($service)) { 379 | return $graph->getVertex($service); 380 | } 381 | 382 | $vertex = $graph->createVertex($service); 383 | $vertex->setAttribute('docker_compose.type', $type ?: 'service'); 384 | 385 | return $vertex; 386 | } 387 | 388 | /** 389 | * @internal 390 | * 391 | * @param Graph $graph Input graph 392 | * @param int $port Port number 393 | * @param string|null $proto Protocol 394 | * 395 | * @return Vertex 396 | */ 397 | function addPort(Graph $graph, int $port, string $proto = null, string $target = null) 398 | { 399 | $target = $target ? $target.':' : null; 400 | 401 | if (true === $graph->hasVertex($target.$port)) { 402 | return $graph->getVertex($target.$port); 403 | } 404 | 405 | $vertex = $graph->createVertex($target.$port); 406 | $vertex->setAttribute('docker_compose.type', 'port'); 407 | $vertex->setAttribute('docker_compose.proto', $proto ?: 'tcp'); 408 | 409 | return $vertex; 410 | } 411 | 412 | /** 413 | * @internal 414 | * 415 | * @param Graph $graph Input graph 416 | * @param string $path Path 417 | * 418 | * @return Vertex 419 | */ 420 | function addVolume(Graph $graph, string $path) 421 | { 422 | if (true === $graph->hasVertex($path)) { 423 | return $graph->getVertex($path); 424 | } 425 | 426 | $vertex = $graph->createVertex($path); 427 | $vertex->setAttribute('docker_compose.type', 'volume'); 428 | 429 | return $vertex; 430 | } 431 | 432 | /** 433 | * @internal 434 | * 435 | * @param Graph $graph Input graph 436 | * @param string $name Name of the network 437 | * @param string $type Network type 438 | * 439 | * @return Vertex 440 | */ 441 | function addNetwork(Graph $graph, string $name, string $type = null) 442 | { 443 | if (true === $graph->hasVertex($name)) { 444 | return $graph->getVertex($name); 445 | } 446 | 447 | $vertex = $graph->createVertex($name); 448 | $vertex->setAttribute('docker_compose.type', $type ?: 'network'); 449 | 450 | return $vertex; 451 | } 452 | 453 | /** 454 | * @internal 455 | * 456 | * @param Vertex $from Source vertex 457 | * @param Vertex $to Destination vertex 458 | * @param string $type Type of the relation (one of "links", "volumes_from", "depends_on", "ports"); 459 | * @param string|null $alias Alias associated to the linked element 460 | * @param bool|null $bidirectional Biderectional or not 461 | * @param bool|null $condition Wether the alias represents a condition or not 462 | */ 463 | function addRelation(Vertex $from, Vertex $to, string $type, string $alias = null, bool $bidirectional = false, bool $condition = false): Edge\Directed 464 | { 465 | $edge = null; 466 | 467 | if ($from->hasEdgeTo($to)) { 468 | $edges = $from->getEdgesTo($to); 469 | 470 | foreach ($edges as $edge) { 471 | if ($edge->getAttribute('docker_compose.type') === $type) { 472 | break; 473 | } 474 | } 475 | } 476 | 477 | if (null === $edge) { 478 | $edge = $from->createEdgeTo($to); 479 | } 480 | 481 | $edge->setAttribute('docker_compose.type', $type); 482 | 483 | if (null !== $alias) { 484 | $edge->setAttribute('docker_compose.alias', $alias); 485 | } 486 | 487 | if (true === $condition) { 488 | $edge->setAttribute('docker_compose.condition', true); 489 | } 490 | 491 | $edge->setAttribute('docker_compose.bidir', $bidirectional); 492 | 493 | return $edge; 494 | } 495 | 496 | /** 497 | * @internal 498 | * 499 | * @param string $mapping A docker mapping ([:]) 500 | * 501 | * @return array An 2 or 3 items array containing the parts of the mapping. 502 | * If the mapping does not specify a second part, the first one will be repeated 503 | */ 504 | function explodeMapping($mapping): array 505 | { 506 | $parts = explode(':', $mapping); 507 | $parts[1] = $parts[1] ?? $parts[0]; 508 | 509 | return [$parts[0], $parts[1]]; 510 | } 511 | 512 | /** 513 | * @internal 514 | * 515 | * @param string $mapping A docker mapping ([:]) 516 | * 517 | * @return array An 2 or 3 items array containing the parts of the mapping. 518 | * If the mapping does not specify a second part, the first one will be repeated 519 | */ 520 | function explodeVolumeMapping($mapping): array 521 | { 522 | $parts = explode(':', $mapping); 523 | $parts[1] = $parts[1] ?? $parts[0]; 524 | 525 | return [$parts[0], $parts[1], $parts[2] ?? null]; 526 | } 527 | 528 | /** 529 | * @internal 530 | * 531 | * @param string $mapping A docker mapping ([:]) 532 | * 533 | * @return array An 2 or 3 items array containing the parts of the mapping. 534 | * If the mapping does not specify a second part, the first one will be repeated 535 | */ 536 | function explodePortMapping($mapping): array 537 | { 538 | $parts = explode(':', $mapping); 539 | 540 | if (count($parts) < 3) { 541 | $target = null; 542 | $host = $parts[0]; 543 | $container = $parts[1] ?? $parts[0]; 544 | } else { 545 | $target = $parts[0]; 546 | $host = $parts[1]; 547 | $container = $parts[2]; 548 | } 549 | 550 | $subparts = array_values(array_filter(explode('/', $container))); 551 | 552 | return [$target, $host, $subparts[0], $subparts[1] ?? null]; 553 | } 554 | --------------------------------------------------------------------------------