├── .gitignore ├── src ├── Exception │ ├── NotFoundException.php │ ├── ToxicExistsException.php │ ├── InvalidProxyException.php │ ├── InvalidToxicException.php │ ├── ProxyExistsException.php │ ├── Exception.php │ └── UnexpectedStatusCodeException.php ├── StreamDirections.php ├── StatusCodes.php ├── ToxicTypes.php ├── Toxic.php ├── UrlHelpers.php ├── Proxy.php └── Toxiproxy.php ├── docker └── php │ ├── xdebug.ini │ └── php.ini ├── phpstan.neon ├── examples ├── composer.json ├── bandwidth.php ├── slow-close.php ├── latency.php └── timeout.php ├── .github └── workflows │ └── ci.yml ├── docker-compose.yml ├── phpcs.xml ├── phpunit.xml.dist ├── Dockerfile ├── tests ├── Test │ ├── HttpMockHelpers.php │ └── BaseTestCase.php ├── ProxyTest.php └── ToxiproxyTest.php ├── LICENSE ├── README.md └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | /_reports 4 | /_docs 5 | /examples/vendor -------------------------------------------------------------------------------- /src/Exception/NotFoundException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | */src/Kernel.php 9 | 10 | -------------------------------------------------------------------------------- /docker/php/php.ini: -------------------------------------------------------------------------------- 1 | ; Recommended production values 2 | display_errors = Off 3 | display_startup_errors = Off 4 | error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT 5 | html_errors = Off 6 | log_errors = On 7 | max_input_time = 60 8 | output_buffering = 4096 9 | register_argc_argv = Off 10 | request_order = "GP" 11 | session.gc_divisor = 1000 12 | short_open_tag = Off 13 | track_errors = Off 14 | variables_order = "GPCS" 15 | 16 | ; Custom 17 | date.timezone = UTC 18 | memory_limit = -1 19 | -------------------------------------------------------------------------------- /examples/bandwidth.php: -------------------------------------------------------------------------------- 1 | create('ihsw_example_redis_master', '127.0.0.1:6379'); 13 | $toxic = $proxy->create(ToxicTypes::BANDWIDTH->value, StreamDirections::UPSTREAM->value, 1.0, [ 14 | 'rate' => 1000, 15 | ]); 16 | printf( 17 | "Listening on IP %s and port %s on behalf of 6379, with a connection that's limited to 1000KB/s\n", 18 | $proxy->getListenIp(), 19 | $proxy->getListenPort(), 20 | ); 21 | 22 | $toxiproxy->delete($proxy); 23 | -------------------------------------------------------------------------------- /examples/slow-close.php: -------------------------------------------------------------------------------- 1 | create('ihsw_example_redis_master', '127.0.0.1:6379'); 13 | $toxic = $proxy->create(ToxicTypes::SLOW_CLOSE->value, StreamDirections::UPSTREAM->value, 1.0, [ 14 | 'delay' => 1000, 15 | ]); 16 | printf( 17 | "Listening on IP %s and port %s on behalf of 6379, with a connection that takes 1000ms to close\n", 18 | $proxy->getListenIp(), 19 | $proxy->getListenPort(), 20 | ); 21 | 22 | $toxiproxy->delete($proxy); 23 | -------------------------------------------------------------------------------- /examples/latency.php: -------------------------------------------------------------------------------- 1 | create('ihsw_example_redis_master', '127.0.0.1:6379'); 13 | $toxic = $proxy->create(ToxicTypes::LATENCY->value, StreamDirections::UPSTREAM->value, 1.0, [ 14 | 'latency' => 100, 15 | 'jitter' => 50, 16 | ]); 17 | printf( 18 | "Listening on IP %s and port %s on behalf of 6379, with latency between 100-150ms\n", 19 | $proxy->getListenIp(), 20 | $proxy->getListenPort(), 21 | ); 22 | 23 | $toxiproxy->delete($proxy); 24 | -------------------------------------------------------------------------------- /examples/timeout.php: -------------------------------------------------------------------------------- 1 | create('ihsw_example_redis_master', '127.0.0.1:6379'); 13 | $toxic = $proxy->create(ToxicTypes::TIMEOUT->value, StreamDirections::UPSTREAM->value, 1.0, [ 14 | 'timeout' => 5*1000, 15 | ]); 16 | printf( 17 | "Listening on IP %s and port %s on behalf of 6379, with a connection that does nothing but timeout after 5000ms\n", 18 | $proxy->getListenIp(), 19 | $proxy->getListenPort(), 20 | ); 21 | 22 | $toxiproxy->delete($proxy); 23 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | 20 | src 21 | 22 | 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.3 2 | 3 | ARG PHP_VERSION=8.4 4 | ARG XDEBUG_VERSION="-3.4.2" 5 | 6 | FROM php:${PHP_VERSION}-cli as dev 7 | 8 | ARG DEBIAN_FRONTEND=noninteractivef 9 | ARG XDEBUG_VERSION 10 | ENV COMPOSER_FLAGS="--prefer-dist --no-interaction" 11 | ENV COMPOSER_ALLOW_SUPERUSER 1 12 | 13 | COPY docker/php/php.ini /usr/local/etc/php/php.ini 14 | COPY docker/php/xdebug.ini /usr/local/etc/php/conf.d/ 15 | 16 | RUN apt update -q \ 17 | && apt install -y --no-install-recommends git zip unzip libzip4 libzip-dev zlib1g-dev \ 18 | && docker-php-ext-install zip \ 19 | && apt-get remove --autoremove -y libzip-dev zlib1g-dev \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin/ --filename=composer 23 | 24 | RUN pecl install xdebug${XDEBUG_VERSION} \ 25 | && docker-php-ext-enable xdebug 26 | 27 | WORKDIR /code 28 | 29 | COPY composer.* /code/ 30 | RUN composer install $COMPOSER_FLAGS --no-scripts 31 | 32 | COPY . /code/ 33 | 34 | -------------------------------------------------------------------------------- /tests/Test/HttpMockHelpers.php: -------------------------------------------------------------------------------- 1 | $responses 16 | */ 17 | protected static function mockHttpClientFactory(?array $responses): HttpClient 18 | { 19 | $mock = new HttpMockHandler($responses); 20 | $handler = HttpHandlerStack::create($mock); 21 | return new HttpClient( 22 | [ 23 | 'handler' => $handler, 24 | 'http_errors' => false, 25 | ], 26 | ); 27 | } 28 | 29 | /** 30 | * @param string[][] $headers 31 | */ 32 | protected static function httpResponseFactory(int $statusCode, string $body, array $headers = []): HttpResponse 33 | { 34 | return new HttpResponse($statusCode, $headers, $body); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ihsw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Toxiproxy PHP Client 2 | ==================== 3 | 4 | [Toxiproxy](https://github.com/shopify/toxiproxy) makes it easy and trivial to test network conditions, for example low-bandwidth and high-latency situations. `toxiproxy-php-client` includes everything needed to get started with configuring Toxiproxy upstream connection and listen endpoints. 5 | 6 | *Note: `toxiproxy-php-client` is currently compatible with `toxiproxy-2.0+`.* 7 | 8 | Installing via Composer 9 | ----------------------- 10 | 11 | The recommended way to install `toxiproxy-php-client` is through [Composer](http://getcomposer.org/). 12 | 13 | Once that is installed and you have added `ihsw/toxiproxy-php-client` to your `composer.json` configuration, you can require the autoloader and start using the library. 14 | 15 | Here is an example for creating a proxy that limits a Redis connection to 1000KB/s. 16 | 17 | ```php 18 | create("ihsw_example_redis_master", "127.0.0.1:6379"); 28 | $toxic = $proxy->create(ToxicTypes::BANDWIDTH->value, StreamDirections::UPSTREAM->value, 1.0, [ 29 | "rate" => 1000 30 | ]); 31 | printf( 32 | "Listening on IP %s and port %s on behalf of 6379, with a connection that's limited to 1000KB/s\n", 33 | $proxy->getListenIp(), 34 | $proxy->getListenPort() 35 | ); 36 | 37 | $toxiproxy->delete($proxy); 38 | 39 | ``` 40 | 41 | Documentation 42 | ------------- 43 | 44 | Additional examples can be found in the `examples` directory for expected usage. 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ihsw/toxiproxy-php-client", 3 | "description": "PHP client for shopify/toxiproxy", 4 | "license": "MIT", 5 | "keywords": [ 6 | "toxiproxy", 7 | "php", 8 | "client" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Adrian Parker", 13 | "email": "ihsw.aparker@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.3", 18 | "ext-json": "*", 19 | "guzzlehttp/guzzle": "^7.9" 20 | }, 21 | "require-dev": { 22 | "keboola/coding-standard": "^15.1", 23 | "phpunit/phpunit": "^12.1", 24 | "phpstan/phpstan": "^2.1", 25 | "phpstan/phpstan-phpunit": "^2.0", 26 | "squizlabs/php_codesniffer": "^3.12" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Ihsw\\Toxiproxy\\": "src" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Ihsw\\Toxiproxy\\Tests\\": "tests" 36 | } 37 | }, 38 | "scripts": { 39 | "ci": [ 40 | "@composer validate --no-check-publish --no-check-all", 41 | "@phpcs", 42 | "@phpstan", 43 | "@phpunit" 44 | ], 45 | "phpcs": "phpcs -n --ignore=vendor,cache,Kernel.php --extensions=php .", 46 | "phpcbf": "phpcbf --extensions=php src tests examples", 47 | "phpstan": "phpstan analyse --no-progress -c phpstan.neon", 48 | "phpunit": [ 49 | "@putenv XDEBUG_MODE=coverage", 50 | "phpunit --coverage-clover /tmp/build-logs/clover.xml --coverage-xml=/tmp/build-logs/coverage-xml --log-junit=/tmp/build-logs/phpunit.junit.xml" 51 | ] 52 | }, 53 | "config": { 54 | "lock": false, 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "dealerdirect/phpcodesniffer-composer-installer": true 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Toxic.php: -------------------------------------------------------------------------------- 1 | proxy = $proxy; 21 | $this->name = $name; 22 | $this->type = $type; 23 | $this->stream = $stream; 24 | $this->toxicity = 1.0; 25 | $this->attributes = []; 26 | } 27 | 28 | public function getName(): string 29 | { 30 | return $this->name; 31 | } 32 | 33 | public function getType(): string 34 | { 35 | return $this->type; 36 | } 37 | 38 | public function getStream(): string 39 | { 40 | return $this->stream; 41 | } 42 | 43 | public function getToxicity(): float 44 | { 45 | return $this->toxicity; 46 | } 47 | 48 | public function getProxy(): Proxy 49 | { 50 | return $this->proxy; 51 | } 52 | 53 | public function setToxicity(float $toxicity): self 54 | { 55 | $this->toxicity = $toxicity; 56 | return $this; 57 | } 58 | 59 | public function getAttributes(): array 60 | { 61 | return $this->attributes; 62 | } 63 | 64 | public function setAttributes(array $attributes): self 65 | { 66 | $this->attributes = $attributes; 67 | return $this; 68 | } 69 | 70 | public function jsonSerialize(): array 71 | { 72 | return [ 73 | 'name' => $this->name, 74 | 'stream' => $this->stream, 75 | 'type' => $this->type, 76 | 'toxicity' => $this->toxicity, 77 | 'attributes' => $this->attributes, 78 | ]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Test/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | createToxiproxy(); 24 | foreach ($toxiproxy->getAll() as $proxy) { 25 | $toxiproxy->delete($proxy); 26 | } 27 | } 28 | 29 | /** 30 | * @param array|null $mockResponses 31 | */ 32 | protected function createToxiproxy(?array $mockResponses = null): Toxiproxy 33 | { 34 | if ($mockResponses === null) { 35 | return new Toxiproxy(sprintf('http://%s:%s', $this->getToxiproxyHost(), $this->getToxiproxyPort())); 36 | } 37 | 38 | $toxiproxy = new Toxiproxy(''); 39 | $toxiproxy->setHttpClient(self::mockHttpClientFactory($mockResponses)); 40 | return $toxiproxy; 41 | } 42 | 43 | private function getToxiproxyHost(): string 44 | { 45 | return (string) getenv('TOXIPROXY_HOST'); 46 | } 47 | 48 | private function getToxiproxyPort(): string 49 | { 50 | return (string) getenv('TOXIPROXY_PORT'); 51 | } 52 | 53 | 54 | protected function getListen(int $listen = 34343): string 55 | { 56 | return sprintf('%s:%s', '127.0.0.1', $listen); 57 | } 58 | 59 | protected function createProxy(Toxiproxy $toxiproxy): Proxy 60 | { 61 | return $toxiproxy->create(self::PROXY_NAME, self::TEST_UPSTREAM, $this->getListen()); 62 | } 63 | 64 | protected function createToxic(Proxy $proxy, string $type, array $attr): Toxic 65 | { 66 | return $proxy->create($type, StreamDirections::UPSTREAM->value, 1.0, $attr); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/UrlHelpers.php: -------------------------------------------------------------------------------- 1 | 'GET', 'uri' => '/proxies']; 16 | } 17 | 18 | /** 19 | * @return array{method: string, uri: string} 20 | */ 21 | public function createProxyRoute(): array 22 | { 23 | return ['method' => 'POST', 'uri' => '/proxies']; 24 | } 25 | 26 | /** 27 | * @return array{method: string, uri: string} 28 | */ 29 | public function populateRoute(): array 30 | { 31 | return ['method' => 'POST', 'uri' => '/populate']; 32 | } 33 | 34 | /** 35 | * @return array{method: string, uri: string} 36 | */ 37 | public function getProxyRoute(string $name): array 38 | { 39 | return ['method' => 'GET', 'uri' => sprintf('/proxies/%s', $name)]; 40 | } 41 | 42 | /** 43 | * @return array{method: string, uri: string} 44 | */ 45 | public function updateProxyRoute(Proxy $proxy): array 46 | { 47 | return ['method' => 'POST', 'uri' => sprintf('/proxies/%s', $proxy->getName())]; 48 | } 49 | 50 | /** 51 | * @return array{method: string, uri: string} 52 | */ 53 | public function deleteProxyRoute(Proxy $proxy): array 54 | { 55 | return ['method' => 'DELETE', 'uri' => sprintf('/proxies/%s', $proxy->getName())]; 56 | } 57 | 58 | /** 59 | * @return array{method: string, uri: string} 60 | */ 61 | public function getToxicsRoute(Proxy $proxy): array 62 | { 63 | return ['method' => 'GET', 'uri' => sprintf('/proxies/%s/toxics', $proxy->getName())]; 64 | } 65 | 66 | /** 67 | * @return array{method: string, uri: string} 68 | */ 69 | public function createToxicRoute(Proxy $proxy): array 70 | { 71 | return ['method' => 'POST', 'uri' => sprintf('/proxies/%s/toxics', $proxy->getName())]; 72 | } 73 | 74 | /** 75 | * @return array{method: string, uri: string} 76 | */ 77 | public function getToxicRoute(Proxy $proxy, string $name): array 78 | { 79 | return ['method' => 'GET', 'uri' => sprintf('/proxies/%s/toxics/%s', $proxy->getName(), $name)]; 80 | } 81 | 82 | /** 83 | * @return array{method: string, uri: string} 84 | */ 85 | public function updateToxicRoute(Proxy $proxy, Toxic $toxic): array 86 | { 87 | return ['method' => 'POST', 'uri' => sprintf('/proxies/%s/toxics/%s', $proxy->getName(), $toxic->getName())]; 88 | } 89 | 90 | /** 91 | * @return array{method: string, uri: string} 92 | */ 93 | public function deleteToxicRoute(Proxy $proxy, Toxic $toxic): array 94 | { 95 | return ['method' => 'DELETE', 'uri' => sprintf('/proxies/%s/toxics/%s', $proxy->getName(), $toxic->getName())]; 96 | } 97 | 98 | /** 99 | * @return array{method: string, uri: string} 100 | */ 101 | public function resetRoute(): array 102 | { 103 | return ['method' => 'POST', 'uri' => '/reset']; 104 | } 105 | 106 | /** 107 | * @return array{method: string, uri: string} 108 | */ 109 | public function versionRoute(): array 110 | { 111 | return ['method' => 'GET', 'uri' => '/version']; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/ProxyTest.php: -------------------------------------------------------------------------------- 1 | createToxiproxy(); 21 | $proxy = $this->createProxy($toxiproxy); 22 | 23 | $attr = [ 24 | 'latency' => 1000, 25 | 'jitter' => 50, 26 | ]; 27 | $toxic = $proxy->create(ToxicTypes::LATENCY->value, StreamDirections::UPSTREAM->value, 1.0, $attr); 28 | self::assertEquals(ToxicTypes::LATENCY->value, $toxic->getType()); 29 | self::assertEquals(StreamDirections::UPSTREAM->value, $toxic->getStream()); 30 | self::assertEquals(1.0, $toxic->getToxicity()); 31 | self::assertEquals($attr, $toxic->getAttributes()); 32 | } 33 | 34 | public function testCreateDuplicate(): void 35 | { 36 | $toxiproxy = $this->createToxiproxy(); 37 | $proxy = $this->createProxy($toxiproxy); 38 | $this->createToxic( 39 | $proxy, 40 | ToxicTypes::LATENCY->value, 41 | [ 42 | 'latency' => 1000, 43 | 'jitter' => 500, 44 | ], 45 | ); 46 | 47 | $this->expectException(ToxicExistsException::class); 48 | $this->createToxic( 49 | $proxy, 50 | ToxicTypes::LATENCY->value, 51 | [ 52 | 'latency' => 1000, 53 | 'jitter' => 500, 54 | ], 55 | ); 56 | } 57 | 58 | public function testCreatedUnexpectedStatus(): void 59 | { 60 | $toxiproxy = $this->createToxiproxy( 61 | [ 62 | self::httpResponseFactory(418, ''), 63 | ], 64 | ); 65 | 66 | $this->expectException(UnexpectedStatusCodeException::class); 67 | $this->createToxic(new Proxy($toxiproxy, ''), '', []); 68 | } 69 | 70 | public function testGetAll(): void 71 | { 72 | $toxiproxy = $this->createToxiproxy(); 73 | $proxy = $this->createProxy($toxiproxy); 74 | $toxic = $this->createToxic( 75 | $proxy, 76 | ToxicTypes::LATENCY->value, 77 | [ 78 | 'latency' => 1000, 79 | 'jitter' => 500, 80 | ], 81 | ); 82 | 83 | $toxics = $proxy->getAll(); 84 | self::assertEquals($toxic, $toxics[0]); 85 | } 86 | 87 | public function testGetAllUnexpectedStatus(): void 88 | { 89 | $toxiproxy = $this->createToxiproxy( 90 | [ 91 | self::httpResponseFactory(418, ''), 92 | ], 93 | ); 94 | 95 | $this->expectException(UnexpectedStatusCodeException::class); 96 | $proxy = new Proxy($toxiproxy, ''); 97 | $proxy->getAll(); 98 | } 99 | 100 | public function testGet(): void 101 | { 102 | $toxiproxy = $this->createToxiproxy(); 103 | $proxy = $this->createProxy($toxiproxy); 104 | $toxic = $this->createToxic( 105 | $proxy, 106 | ToxicTypes::LATENCY->value, 107 | [ 108 | 'latency' => 1000, 109 | 'jitter' => 500, 110 | ], 111 | ); 112 | 113 | self::assertEquals($toxic, $proxy->get($toxic->getName())); 114 | } 115 | 116 | public function testGetNotFound(): void 117 | { 118 | $toxiproxy = $this->createToxiproxy(); 119 | $proxy = $this->createProxy($toxiproxy); 120 | 121 | $toxic = $proxy->get('non-existent'); 122 | self::assertNull($toxic); 123 | } 124 | 125 | public function testGetUnexpectedStatus(): void 126 | { 127 | $toxiproxy = $this->createToxiproxy( 128 | [ 129 | self::httpResponseFactory(418, ''), 130 | ], 131 | ); 132 | 133 | $this->expectException(UnexpectedStatusCodeException::class); 134 | $proxy = new Proxy($toxiproxy, ''); 135 | $proxy->get(''); 136 | } 137 | 138 | public function testUpdate(): void 139 | { 140 | $toxiproxy = $this->createToxiproxy(); 141 | $proxy = $this->createProxy($toxiproxy); 142 | $toxic = $this->createToxic( 143 | $proxy, 144 | ToxicTypes::LATENCY->value, 145 | [ 146 | 'latency' => 1000, 147 | 'jitter' => 500, 148 | ], 149 | ); 150 | 151 | $toxic->setAttributes(['latency' => 2000, 'jitter' => 1000]) 152 | ->setToxicity(0.5); 153 | $updatedToxic = $proxy->update($toxic); 154 | self::assertEquals($updatedToxic, $toxic); 155 | } 156 | 157 | public function testUpdateNotFound(): void 158 | { 159 | $toxiproxy = $this->createToxiproxy(); 160 | $proxy = $this->createProxy($toxiproxy); 161 | $toxic = $this->createToxic( 162 | $proxy, 163 | ToxicTypes::LATENCY->value, 164 | [ 165 | 'latency' => 1000, 166 | 'jitter' => 500, 167 | ], 168 | ); 169 | 170 | $proxy->delete($toxic); 171 | $this->expectException(NotFoundException::class); 172 | $proxy->update($toxic); 173 | } 174 | 175 | public function testUpdateUnexpectedStatus(): void 176 | { 177 | $toxiproxy = $this->createToxiproxy( 178 | [ 179 | self::httpResponseFactory(418, ''), 180 | ], 181 | ); 182 | 183 | $this->expectException(UnexpectedStatusCodeException::class); 184 | $proxy = new Proxy($toxiproxy, ''); 185 | $proxy->update(new Toxic($proxy, '', '', '')); 186 | } 187 | 188 | public function testDelete(): void 189 | { 190 | $toxiproxy = $this->createToxiproxy(); 191 | $proxy = $this->createProxy($toxiproxy); 192 | $toxic = $this->createToxic( 193 | $proxy, 194 | ToxicTypes::LATENCY->value, 195 | [ 196 | 'latency' => 1000, 197 | 'jitter' => 500, 198 | ], 199 | ); 200 | self::assertCount(1, $toxiproxy->getAll()); 201 | self::assertCount(1, $proxy->getAll()); 202 | $proxy->delete($toxic); 203 | self::assertCount(1, $toxiproxy->getAll()); 204 | self::assertCount(0, $proxy->getAll()); 205 | } 206 | 207 | public function testDeleteNotFound(): void 208 | { 209 | $toxiproxy = $this->createToxiproxy(); 210 | $proxy = $this->createProxy($toxiproxy); 211 | $toxic = $this->createToxic( 212 | $proxy, 213 | ToxicTypes::LATENCY->value, 214 | [ 215 | 'latency' => 1000, 216 | 'jitter' => 500, 217 | ], 218 | ); 219 | 220 | $proxy->delete($toxic); 221 | $this->expectException(NotFoundException::class); 222 | $proxy->delete($toxic); 223 | } 224 | 225 | public function testDeleteUnexpectedStatus(): void 226 | { 227 | $toxiproxy = $this->createToxiproxy( 228 | [ 229 | self::httpResponseFactory(418, ''), 230 | ], 231 | ); 232 | 233 | $this->expectException(UnexpectedStatusCodeException::class); 234 | $proxy = new Proxy($toxiproxy, ''); 235 | $proxy->delete(new Toxic($proxy, '', '', '')); 236 | } 237 | 238 | public function testGetListenIp(): void 239 | { 240 | $toxiproxy = $this->createToxiproxy(); 241 | $proxy = $this->createProxy($toxiproxy); 242 | $listenIp = $proxy->getListenIp(); 243 | self::assertNotEmpty($listenIp); 244 | } 245 | 246 | public function testGetListenPort(): void 247 | { 248 | $toxiproxy = $this->createToxiproxy(); 249 | $proxy = $this->createProxy($toxiproxy); 250 | $listenPort = $proxy->getListenPort(); 251 | self::assertNotEmpty($listenPort); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /tests/ToxiproxyTest.php: -------------------------------------------------------------------------------- 1 | createToxiproxy(); 20 | $listen = $this->getListen(); 21 | $proxy = $toxiproxy->create(self::PROXY_NAME, self::TEST_UPSTREAM, $listen); 22 | 23 | self::assertEquals(self::PROXY_NAME, $proxy->getName()); 24 | self::assertEquals(self::TEST_UPSTREAM, $proxy->getUpstream()); 25 | self::assertEquals($listen, $proxy->getListen()); 26 | self::assertTrue($proxy->isEnabled()); 27 | 28 | $toxiproxy->delete($proxy); 29 | } 30 | 31 | public function testCreateDuplicate(): void 32 | { 33 | $toxiproxy = $this->createToxiproxy(); 34 | $this->createProxy($toxiproxy); 35 | 36 | $this->expectException(ProxyExistsException::class); 37 | $this->createProxy($toxiproxy); 38 | } 39 | 40 | public function testCreateUnexpectedStatus(): void 41 | { 42 | $toxiproxy = $this->createToxiproxy( 43 | [ 44 | self::httpResponseFactory(418, ''), 45 | ], 46 | ); 47 | 48 | $this->expectException(UnexpectedStatusCodeException::class); 49 | $this->createProxy($toxiproxy); 50 | } 51 | 52 | public function testPopulate(): void 53 | { 54 | $toxiproxy = $this->createToxiproxy(); 55 | 56 | // producing a list of proposed proxy-bodies and the expected proxies 57 | $proxyBodies = [ 58 | [ 59 | 'name' => self::PROXY_NAME, 60 | 'listen' => $this->getListen(), 61 | 'upstream' => self::TEST_UPSTREAM, 62 | 'enabled' => true, 63 | ], 64 | ]; 65 | $expectedProxy = new Proxy($toxiproxy, $proxyBodies[0]['name']); 66 | $expectedProxy->setEnabled($proxyBodies[0]['enabled']) 67 | ->setListen($proxyBodies[0]['listen']) 68 | ->setUpstream($proxyBodies[0]['upstream']); 69 | $expectedProxies = [$expectedProxy]; 70 | 71 | // populating 72 | $proxies = $toxiproxy->populate($proxyBodies); 73 | 74 | // comparing the results 75 | self::assertEquals($expectedProxies, $proxies); 76 | } 77 | 78 | public function testPopulateBadData(): void 79 | { 80 | $toxiproxy = $this->createToxiproxy(); 81 | 82 | $this->expectException(InvalidProxyException::class); 83 | $toxiproxy->populate(['bullshit' => 'yes']); 84 | } 85 | 86 | public function testPopulateUnexpectedStatus(): void 87 | { 88 | $toxiproxy = $this->createToxiproxy( 89 | [ 90 | self::httpResponseFactory(418, ''), 91 | ], 92 | ); 93 | 94 | $this->expectException(UnexpectedStatusCodeException::class); 95 | $toxiproxy->populate([]); 96 | } 97 | 98 | public function testGet(): void 99 | { 100 | $toxiproxy = $this->createToxiproxy(); 101 | $proxy = $this->createProxy($toxiproxy); 102 | 103 | self::assertEquals( 104 | $proxy->getName(), 105 | $toxiproxy->get($proxy->getName())?->getName(), 106 | ); 107 | } 108 | 109 | public function testGetUnexpectedStatus(): void 110 | { 111 | $toxiproxy = $this->createToxiproxy( 112 | [ 113 | self::httpResponseFactory(418, ''), 114 | ], 115 | ); 116 | 117 | $this->expectException(UnexpectedStatusCodeException::class); 118 | $toxiproxy->get(''); 119 | } 120 | 121 | public function testGetWithToxics(): void 122 | { 123 | $toxiproxy = $this->createToxiproxy(); 124 | $proxy = $this->createProxy($toxiproxy); 125 | 126 | $toxics = [$this->createToxic( 127 | $proxy, 128 | ToxicTypes::LATENCY->value, 129 | [ 130 | 'latency' => 1000, 131 | 'jitter' => 500, 132 | ], 133 | )]; 134 | $receivedProxy = $toxiproxy->get($proxy->getName()); 135 | self::assertSame( 136 | $toxics[0]->jsonSerialize(), 137 | $receivedProxy?->getToxics()[0]->jsonSerialize(), 138 | ); 139 | } 140 | 141 | public function testGetNotFound(): void 142 | { 143 | $toxiproxy = $this->createToxiproxy(); 144 | $proxy = $toxiproxy->get('non-existent'); 145 | self::assertNull($proxy); 146 | } 147 | 148 | public function testGetAll(): void 149 | { 150 | $toxiproxy = $this->createToxiproxy(); 151 | $proxy = $this->createProxy($toxiproxy); 152 | $proxies = $toxiproxy->getAll(); 153 | self::assertEquals($proxy, $proxies[0]); 154 | } 155 | 156 | public function testGetAllUnexpectedStatus(): void 157 | { 158 | $toxiproxy = $this->createToxiproxy( 159 | [ 160 | self::httpResponseFactory(418, ''), 161 | ], 162 | ); 163 | 164 | $this->expectException(UnexpectedStatusCodeException::class); 165 | $toxiproxy->getAll(); 166 | } 167 | 168 | public function testDelete(): void 169 | { 170 | $toxiproxy = $this->createToxiproxy(); 171 | self::assertCount(0, $toxiproxy->getAll()); 172 | $toxiproxy->delete($this->createProxy($toxiproxy)); 173 | self::assertCount(0, $toxiproxy->getAll()); 174 | } 175 | 176 | public function testDeleteNotFound(): void 177 | { 178 | $toxiproxy = $this->createToxiproxy(); 179 | $proxy = $this->createProxy($toxiproxy); 180 | $toxiproxy->delete($proxy); 181 | 182 | $this->expectException(NotFoundException::class); 183 | $toxiproxy->delete($proxy); 184 | } 185 | 186 | public function testDeleteUnexpectedStatus(): void 187 | { 188 | $toxiproxy = $this->createToxiproxy( 189 | [ 190 | self::httpResponseFactory(418, ''), 191 | ], 192 | ); 193 | 194 | $this->expectException(UnexpectedStatusCodeException::class); 195 | $toxiproxy->delete(new Proxy($toxiproxy, '')); 196 | } 197 | 198 | public function testUpdate(): void 199 | { 200 | $toxiproxy = $this->createToxiproxy(); 201 | $proxy = $this->createProxy($toxiproxy); 202 | 203 | $proxy->setListen($this->getListen(43434)) 204 | ->setUpstream(self::TEST_UPSTREAM) 205 | ->setEnabled(false); 206 | $updatedProxy = $toxiproxy->update($proxy); 207 | self::assertEquals($proxy, $updatedProxy); 208 | } 209 | 210 | public function testUpdateDisabled(): void 211 | { 212 | $toxiproxy = $this->createToxiproxy(); 213 | 214 | $proxy = $this->createProxy($toxiproxy); 215 | self::assertTrue($proxy->isEnabled()); 216 | 217 | $proxy->setEnabled(false); 218 | $proxy = $toxiproxy->update($proxy); 219 | self::assertFalse($proxy->isEnabled()); 220 | } 221 | 222 | public function testUpdateNotFound(): void 223 | { 224 | $toxiproxy = $this->createToxiproxy(); 225 | 226 | $this->expectException(NotFoundException::class); 227 | $toxiproxy->update(new Proxy($toxiproxy, 'not-found')); 228 | } 229 | 230 | public function testUpdateUnexpectedStatus(): void 231 | { 232 | $toxiproxy = $this->createToxiproxy( 233 | [ 234 | self::httpResponseFactory(418, ''), 235 | ], 236 | ); 237 | 238 | $this->expectException(UnexpectedStatusCodeException::class); 239 | $toxiproxy->update(new Proxy($toxiproxy, '')); 240 | } 241 | 242 | public function testReset(): void 243 | { 244 | $toxiproxy = $this->createToxiproxy(); 245 | 246 | // creating a proxy and a toxic, and disabling the proxy 247 | $proxy = $this->createProxy($toxiproxy); 248 | $this->createToxic( 249 | $proxy, 250 | ToxicTypes::LATENCY->value, 251 | [ 252 | 'latency' => 1000, 253 | 'jitter' => 500, 254 | ], 255 | ); 256 | $proxy->setEnabled(false); 257 | $toxiproxy->update($proxy); 258 | 259 | // resetting all proxies 260 | $toxiproxy->reset(); 261 | 262 | // checking that this proxy is now re-enabled 263 | $proxy = $toxiproxy->get($proxy->getName()); 264 | self::assertNotNull($proxy); 265 | self::assertTrue($proxy->isEnabled()); 266 | 267 | // checking that this proxy has no toxics 268 | $toxics = $proxy->getAll(); 269 | self::assertEmpty($toxics); 270 | } 271 | 272 | public function testResetUnexpectedStatus(): void 273 | { 274 | $toxiproxy = $this->createToxiproxy( 275 | [ 276 | self::httpResponseFactory(418, ''), 277 | ], 278 | ); 279 | 280 | $this->expectException(UnexpectedStatusCodeException::class); 281 | $toxiproxy->reset(); 282 | } 283 | 284 | public function testVersion(): void 285 | { 286 | $toxiproxy = $this->createToxiproxy(); 287 | 288 | self::assertNotEmpty($toxiproxy->version()); 289 | } 290 | 291 | public function testVersionUnexpectedStatus(): void 292 | { 293 | $toxiproxy = $this->createToxiproxy( 294 | [ 295 | self::httpResponseFactory(418, ''), 296 | ], 297 | ); 298 | 299 | $this->expectException(UnexpectedStatusCodeException::class); 300 | $toxiproxy->version(); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/Proxy.php: -------------------------------------------------------------------------------- 1 | 35 | * }[] $toxicContents 36 | */ 37 | public function __construct(Toxiproxy $toxiproxy, string $name, array $toxicContents = []) 38 | { 39 | $this->toxiproxy = $toxiproxy; 40 | $this->name = $name; 41 | $this->toxics = array_map( 42 | function ($toxicContent) { 43 | return $this->contentsToToxic($toxicContent); 44 | }, 45 | $toxicContents, 46 | ); 47 | $this->enabled = false; 48 | $this->upstream = ''; 49 | $this->listen = ''; 50 | } 51 | 52 | public function getName(): string 53 | { 54 | return $this->name; 55 | } 56 | 57 | public function setListen(string $listen): self 58 | { 59 | $this->listen = $listen; 60 | return $this; 61 | } 62 | 63 | public function getListen(): string 64 | { 65 | return $this->listen; 66 | } 67 | 68 | public function getUpstream(): string 69 | { 70 | return $this->upstream; 71 | } 72 | 73 | public function setUpstream(string $upstream): self 74 | { 75 | $this->upstream = $upstream; 76 | return $this; 77 | } 78 | 79 | public function isEnabled(): bool 80 | { 81 | return $this->enabled; 82 | } 83 | 84 | public function setEnabled(bool $enabled): self 85 | { 86 | $this->enabled = $enabled; 87 | return $this; 88 | } 89 | 90 | /** 91 | * @return Toxic[] 92 | */ 93 | public function getToxics(): array 94 | { 95 | return $this->toxics; 96 | } 97 | 98 | public function jsonSerialize(): array 99 | { 100 | return [ 101 | 'name' => $this->name, 102 | 'listen' => $this->listen, 103 | 'upstream' => $this->upstream, 104 | 'enabled' => $this->enabled, 105 | ]; 106 | } 107 | 108 | private function responseToToxic(ResponseInterface $response): Toxic 109 | { 110 | /** 111 | * @var array{ 112 | * name: string, 113 | * type: string, 114 | * stream: string, 115 | * toxicity: float, 116 | * attributes: array 117 | * } $toxicContents 118 | */ 119 | $toxicContents = (array) json_decode((string) $response->getBody(), true); 120 | return $this->contentsToToxic($toxicContents); 121 | } 122 | 123 | /** 124 | * @param array{ 125 | * name: string, 126 | * type: string, 127 | * stream: string, 128 | * toxicity: float, 129 | * attributes: array 130 | * } $contents 131 | */ 132 | private function contentsToToxic(array $contents): Toxic 133 | { 134 | $toxic = new Toxic( 135 | $this, 136 | $contents['name'], 137 | $contents['type'], 138 | $contents['stream'], 139 | ); 140 | $toxic->setToxicity($contents['toxicity']) 141 | ->setAttributes($contents['attributes']); 142 | 143 | return $toxic; 144 | } 145 | 146 | private function getHttpClient(): Client 147 | { 148 | return $this->toxiproxy->getHttpClient(); 149 | } 150 | 151 | /** 152 | * @return Toxic[] 153 | */ 154 | public function getAll(): array 155 | { 156 | $route = $this->getToxicsRoute($this); 157 | $response = $this->getHttpClient()->request((string) $route['method'], (string) $route['uri']); 158 | switch ($response->getStatusCode()) { 159 | case StatusCodes::OK->value: 160 | /** 161 | * @var array{ 162 | * name: string, 163 | * type: string, 164 | * stream: string, 165 | * toxicity: float, 166 | * attributes: array 167 | * }[] $body 168 | */ 169 | $body = (array) json_decode((string) $response->getBody(), true); 170 | 171 | return array_map( 172 | function ($contents) { 173 | return $this->contentsToToxic($contents); 174 | }, 175 | array_values($body), 176 | ); 177 | default: 178 | throw new UnexpectedStatusCodeException( 179 | sprintf( 180 | '%s: %s', 181 | $response->getStatusCode(), 182 | $response->getBody(), 183 | ), 184 | ); 185 | } 186 | } 187 | 188 | /** 189 | * @throws ToxicExistsException|UnexpectedStatusCodeException 190 | */ 191 | public function create( 192 | string $type, 193 | string $stream, 194 | float $toxicity, 195 | array $attributes, 196 | ?string $name = null, 197 | ): Toxic { 198 | $route = $this->createToxicRoute($this); 199 | $response = $this->getHttpClient()->request( 200 | (string) $route['method'], 201 | (string) $route['uri'], 202 | [ 203 | 'body' => json_encode( 204 | [ 205 | 'name' => $name, 206 | 'stream' => $stream, 207 | 'type' => $type, 208 | 'toxicity' => $toxicity, 209 | 'attributes' => $attributes, 210 | ], 211 | ), 212 | ], 213 | ); 214 | switch ($response->getStatusCode()) { 215 | case StatusCodes::OK->value: 216 | case StatusCodes::NO_CONTENT->value: 217 | return $this->responseToToxic($response); 218 | case StatusCodes::CONFLICT->value: 219 | throw new ToxicExistsException((string) $response->getBody()); 220 | default: 221 | throw new UnexpectedStatusCodeException( 222 | sprintf( 223 | '%s: %s', 224 | $response->getStatusCode(), 225 | $response->getBody(), 226 | ), 227 | ); 228 | } 229 | } 230 | 231 | /** 232 | * @throws UnexpectedStatusCodeException 233 | */ 234 | public function get(string $name): ?Toxic 235 | { 236 | $route = $this->getToxicRoute($this, $name); 237 | $response = $this->getHttpClient()->request((string) $route['method'], (string) $route['uri']); 238 | switch ($response->getStatusCode()) { 239 | case StatusCodes::OK->value: 240 | return $this->responseToToxic($response); 241 | case StatusCodes::NOT_FOUND->value: 242 | return null; 243 | default: 244 | throw new UnexpectedStatusCodeException( 245 | sprintf( 246 | '%s: %s', 247 | $response->getStatusCode(), 248 | $response->getBody(), 249 | ), 250 | ); 251 | } 252 | } 253 | 254 | /** 255 | * @throws NotFoundException|UnexpectedStatusCodeException 256 | */ 257 | public function update(Toxic $toxic): Toxic 258 | { 259 | $route = $this->updateToxicRoute($this, $toxic); 260 | $response = $this->getHttpClient()->request( 261 | (string) $route['method'], 262 | (string) $route['uri'], 263 | ['body' => json_encode($toxic)], 264 | ); 265 | switch ($response->getStatusCode()) { 266 | case StatusCodes::OK->value: 267 | return $this->responseToToxic($response); 268 | case StatusCodes::NOT_FOUND->value: 269 | throw new NotFoundException((string) $response->getBody()); 270 | default: 271 | throw new UnexpectedStatusCodeException( 272 | sprintf( 273 | '%s: %s', 274 | $response->getStatusCode(), 275 | $response->getBody(), 276 | ), 277 | ); 278 | } 279 | } 280 | 281 | /** 282 | * @throws NotFoundException|UnexpectedStatusCodeException 283 | */ 284 | public function delete(Toxic $toxic): void 285 | { 286 | $route = $this->deleteToxicRoute($this, $toxic); 287 | $response = $this->getHttpClient()->request((string) $route['method'], (string) $route['uri']); 288 | switch ($response->getStatusCode()) { 289 | case StatusCodes::NO_CONTENT->value: 290 | return; 291 | case StatusCodes::NOT_FOUND->value: 292 | throw new NotFoundException((string) $response->getBody()); 293 | default: 294 | throw new UnexpectedStatusCodeException( 295 | sprintf( 296 | '%s: %s', 297 | $response->getStatusCode(), 298 | $response->getBody(), 299 | ), 300 | ); 301 | } 302 | } 303 | 304 | public function getListenIp(): string 305 | { 306 | $ip = implode(':', explode(':', $this->listen, -1)); 307 | if (str_starts_with($ip, '[')) { 308 | $ip = substr($ip, 1, -1); 309 | } 310 | return $ip; 311 | } 312 | 313 | public function getListenPort(): string 314 | { 315 | $ip = $this->getListenIp(); 316 | $start = str_starts_with($this->listen, '[') ? 3 : 1; 317 | return substr($this->listen, $start + strlen($ip)); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/Toxiproxy.php: -------------------------------------------------------------------------------- 1 | httpClient = new Client( 24 | [ 25 | 'base_uri' => $baseUrl, 26 | 'http_errors' => false, 27 | ], 28 | ); 29 | } 30 | 31 | public function getHttpClient(): Client 32 | { 33 | return $this->httpClient; 34 | } 35 | 36 | public function setHttpClient(Client $client): self 37 | { 38 | $this->httpClient = $client; 39 | return $this; 40 | } 41 | 42 | private function responseToProxy(ResponseInterface $response): Proxy 43 | { 44 | /** 45 | * @var array{ 46 | * name: string, 47 | * enabled: bool, 48 | * upstream: string, 49 | * listen: string, 50 | * toxics: array{ 51 | * name: string, 52 | * type: string, 53 | * stream: string, 54 | * toxicity: float, 55 | * attributes: array 56 | * }[] 57 | * } $responseBody 58 | */ 59 | $responseBody = (array) json_decode((string) $response->getBody(), true); 60 | return $this->contentsToProxy($responseBody); 61 | } 62 | 63 | /** 64 | * @param array{ 65 | * name: string, 66 | * enabled: bool, 67 | * upstream: string, 68 | * listen: string, 69 | * toxics: array{ 70 | * name: string, 71 | * type: string, 72 | * stream: string, 73 | * toxicity: float, 74 | * attributes: array 75 | * }[] 76 | * } $contents 77 | */ 78 | private function contentsToProxy(array $contents): Proxy 79 | { 80 | $proxy = new Proxy($this, $contents['name'], $contents['toxics']); 81 | $proxy->setEnabled($contents['enabled']) 82 | ->setUpstream($contents['upstream']) 83 | ->setListen($contents['listen']); 84 | 85 | return $proxy; 86 | } 87 | 88 | /** 89 | * @throws ProxyExistsException|UnexpectedStatusCodeException 90 | */ 91 | public function create(string $name, string $upstream, ?string $listen = null): Proxy 92 | { 93 | $route = $this->createProxyRoute(); 94 | $response = $this->httpClient->request( 95 | $route['method'], 96 | $route['uri'], 97 | [ 98 | 'body' => json_encode(['name' => $name, 'upstream' => $upstream, 'listen' => $listen]), 99 | ], 100 | ); 101 | switch ($response->getStatusCode()) { 102 | case StatusCodes::CREATED->value: 103 | return $this->responseToProxy($response); 104 | case StatusCodes::CONFLICT->value: 105 | throw new ProxyExistsException((string) $response->getBody()); 106 | default: 107 | throw new UnexpectedStatusCodeException( 108 | sprintf( 109 | '%s: %s', 110 | $response->getStatusCode(), 111 | $response->getBody(), 112 | ), 113 | ); 114 | } 115 | } 116 | 117 | /** 118 | * @return Proxy[] 119 | * @throws InvalidProxyException|UnexpectedStatusCodeException 120 | */ 121 | public function populate(array $proxyBodies): array 122 | { 123 | $route = $this->populateRoute(); 124 | $response = $this->httpClient->request( 125 | $route['method'], 126 | $route['uri'], 127 | [ 128 | 'body' => json_encode($proxyBodies), 129 | ], 130 | ); 131 | switch ($response->getStatusCode()) { 132 | case StatusCodes::CREATED->value: 133 | /** 134 | * @var array{ 135 | * proxies: array{ 136 | * name: string, 137 | * enabled: bool, 138 | * upstream: string, 139 | * listen: string, 140 | * toxics: array{ 141 | * name: string, 142 | * type: string, 143 | * stream: string, 144 | * toxicity: float, 145 | * attributes: array 146 | * }[] 147 | * }[] 148 | * } $contents 149 | */ 150 | $contents = json_decode((string) $response->getBody(), true); 151 | 152 | return array_map( 153 | fn(array $contents) => $this->contentsToProxy($contents), 154 | $contents['proxies'], 155 | ); 156 | case StatusCodes::BAD_REQUEST->value: 157 | throw new InvalidProxyException((string) $response->getBody()); 158 | default: 159 | throw new UnexpectedStatusCodeException( 160 | sprintf( 161 | '%s: %s', 162 | $response->getStatusCode(), 163 | $response->getBody(), 164 | ), 165 | ); 166 | } 167 | } 168 | 169 | /** 170 | * @throws UnexpectedStatusCodeException 171 | */ 172 | public function get(string $name): ?Proxy 173 | { 174 | $route = $this->getProxyRoute($name); 175 | $response = $this->httpClient->request((string) $route['method'], (string) $route['uri']); 176 | switch ($response->getStatusCode()) { 177 | case StatusCodes::OK->value: 178 | return $this->responseToProxy($response); 179 | case StatusCodes::NOT_FOUND->value: 180 | return null; 181 | default: 182 | throw new UnexpectedStatusCodeException( 183 | sprintf( 184 | '%s: %s', 185 | $response->getStatusCode(), 186 | $response->getBody(), 187 | ), 188 | ); 189 | } 190 | } 191 | 192 | /** 193 | * @return Proxy[] 194 | * @throws UnexpectedStatusCodeException 195 | */ 196 | public function getAll(): array 197 | { 198 | $route = $this->getProxiesRoute(); 199 | $response = $this->httpClient->request($route['method'], $route['uri']); 200 | switch ($response->getStatusCode()) { 201 | case StatusCodes::OK->value: 202 | /** 203 | * @var array{ 204 | * name: string, 205 | * enabled: bool, 206 | * upstream: string, 207 | * listen: string, 208 | * toxics: array{ 209 | * name: string, 210 | * type: string, 211 | * stream: string, 212 | * toxicity: float, 213 | * attributes: array 214 | * }[] 215 | * }[] $body 216 | */ 217 | $body = (array) json_decode((string) $response->getBody(), true); 218 | 219 | return array_map( 220 | fn(array $contents) => $this->contentsToProxy($contents), 221 | array_values($body), 222 | ); 223 | default: 224 | throw new UnexpectedStatusCodeException( 225 | sprintf( 226 | '%s: %s', 227 | $response->getStatusCode(), 228 | $response->getBody(), 229 | ), 230 | ); 231 | } 232 | } 233 | 234 | /** 235 | * @throws NotFoundException|UnexpectedStatusCodeException 236 | */ 237 | public function delete(Proxy $proxy): void 238 | { 239 | $route = $this->deleteProxyRoute($proxy); 240 | $response = $this->httpClient->request((string) $route['method'], (string) $route['uri']); 241 | switch ($response->getStatusCode()) { 242 | case StatusCodes::NO_CONTENT->value: 243 | return; 244 | case StatusCodes::NOT_FOUND->value: 245 | throw new NotFoundException((string) $response->getBody()); 246 | default: 247 | throw new UnexpectedStatusCodeException( 248 | sprintf( 249 | '%s: %s', 250 | $response->getStatusCode(), 251 | $response->getBody(), 252 | ), 253 | ); 254 | } 255 | } 256 | 257 | /** 258 | * @throws NotFoundException|UnexpectedStatusCodeException 259 | */ 260 | public function update(Proxy $proxy): Proxy 261 | { 262 | $route = $this->updateProxyRoute($proxy); 263 | $response = $this->httpClient->request( 264 | (string) $route['method'], 265 | (string) $route['uri'], 266 | [ 267 | 'body' => json_encode($proxy), 268 | ], 269 | ); 270 | switch ($response->getStatusCode()) { 271 | case StatusCodes::OK->value: 272 | return $this->responseToProxy($response); 273 | case StatusCodes::NOT_FOUND->value: 274 | throw new NotFoundException((string) $response->getBody()); 275 | default: 276 | throw new UnexpectedStatusCodeException( 277 | sprintf( 278 | '%s: %s', 279 | $response->getStatusCode(), 280 | $response->getBody(), 281 | ), 282 | ); 283 | } 284 | } 285 | 286 | /** 287 | * @throws UnexpectedStatusCodeException 288 | */ 289 | public function reset(): void 290 | { 291 | $route = $this->resetRoute(); 292 | $response = $this->httpClient->request($route['method'], $route['uri']); 293 | switch ($response->getStatusCode()) { 294 | case StatusCodes::NO_CONTENT->value: 295 | return; 296 | default: 297 | throw new UnexpectedStatusCodeException( 298 | sprintf('Unexpected status code: %s', $response->getStatusCode()), 299 | ); 300 | } 301 | } 302 | 303 | /** 304 | * @throws UnexpectedStatusCodeException 305 | */ 306 | public function version(): string 307 | { 308 | $route = $this->versionRoute(); 309 | $response = $this->httpClient->request($route['method'], $route['uri']); 310 | switch ($response->getStatusCode()) { 311 | case StatusCodes::OK->value: 312 | return (string) $response->getBody(); 313 | default: 314 | throw new UnexpectedStatusCodeException( 315 | sprintf( 316 | '%s: %s', 317 | $response->getStatusCode(), 318 | $response->getBody(), 319 | ), 320 | ); 321 | } 322 | } 323 | } 324 | --------------------------------------------------------------------------------