├── LICENSE ├── composer.json ├── docker-compose.yml ├── phpstan-baseline.neon ├── renovate.json └── src ├── Exception.php └── Protocol ├── Buffer.php ├── NotEnoughBytesAllocated.php ├── Schema.php ├── Schema └── Field.php ├── SchemaValidationFailure.php ├── Type.php ├── Type ├── ArrayOf.php ├── Boolean.php ├── Int16.php ├── Int32.php ├── Int64.php ├── Int8.php ├── NonNullableBytes.php ├── NonNullableString.php ├── NullableBytes.php ├── NullableString.php └── UnsignedInt32.php └── ValueOutOfAllowedRange.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Luís Cobucci 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lcobucci/kafka", 3 | "description": "PHP client for Kafka", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Luís Cobucci", 9 | "email": "lcobucci@gmail.com" 10 | } 11 | ], 12 | "config": { 13 | "preferred-install": "dist", 14 | "sort-packages": true, 15 | "allow-plugins": { 16 | "dealerdirect/phpcodesniffer-composer-installer": true, 17 | "infection/extension-installer": true, 18 | "phpstan/extension-installer": true 19 | } 20 | }, 21 | "require": { 22 | "php-64bit": "^8.1", 23 | "ext-pcntl": "*", 24 | "psr/log": "^3.0", 25 | "react/socket": "^1.13.0" 26 | }, 27 | "require-dev": { 28 | "infection/infection": "^0.27", 29 | "lcobucci/coding-standard": "^11.0.0", 30 | "monolog/monolog": "^3.4.0", 31 | "phpstan/extension-installer": "^1.3.1", 32 | "phpstan/phpstan": "^1.10.24", 33 | "phpstan/phpstan-deprecation-rules": "^1.1.3", 34 | "phpstan/phpstan-phpunit": "^1.3.13", 35 | "phpstan/phpstan-strict-rules": "^1.5.1", 36 | "phpunit/phpunit": "^9.6.9" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Lcobucci\\Kafka\\": "src/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Lcobucci\\Kafka\\Test\\": "tests/" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | zookeeper: 3 | image: bitnami/zookeeper:latest 4 | environment: 5 | - ALLOW_ANONYMOUS_LOGIN=yes 6 | 7 | kafka1: 8 | image: bitnami/kafka:2.8.1 9 | ports: 10 | - "9093:9093" 11 | environment: 12 | - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 13 | - ALLOW_PLAINTEXT_LISTENER=yes 14 | - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT 15 | - KAFKA_CFG_LISTENERS=CLIENT://:9092,EXTERNAL://:9093 16 | - KAFKA_CFG_ADVERTISED_LISTENERS=CLIENT://kafka1:9092,EXTERNAL://localhost:9093 17 | - KAFKA_INTER_BROKER_LISTENER_NAME=CLIENT 18 | 19 | kafka2: 20 | image: bitnami/kafka:2.8.1 21 | profiles: 22 | - clustered 23 | ports: 24 | - "9094:9094" 25 | environment: 26 | - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 27 | - ALLOW_PLAINTEXT_LISTENER=yes 28 | - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT 29 | - KAFKA_CFG_LISTENERS=CLIENT://:9092,EXTERNAL://:9094 30 | - KAFKA_CFG_ADVERTISED_LISTENERS=CLIENT://kafka2:9092,EXTERNAL://localhost:9094 31 | - KAFKA_INTER_BROKER_LISTENER_NAME=CLIENT 32 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Parameter \\#2 \\$data of method Lcobucci\\\\Kafka\\\\Protocol\\\\Buffer\\:\\:unpack\\(\\) expects string, mixed given\\.$#" 5 | count: 5 6 | path: src/Protocol/Buffer.php 7 | 8 | - 9 | message: "#^Parameter \\#1 \\$data of method Lcobucci\\\\Kafka\\\\Protocol\\\\Schema\\:\\:validateField\\(\\) expects array\\, mixed given\\.$#" 10 | count: 1 11 | path: src/Protocol/Schema.php 12 | 13 | - 14 | message: "#^Property Lcobucci\\\\Kafka\\\\Protocol\\\\Schema\\:\\:\\$fields \\(array\\\\) does not accept array\\\\.$#" 15 | count: 1 16 | path: src/Protocol/Schema.php 17 | 18 | - 19 | message: "#^Parameter \\#3 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#" 20 | count: 1 21 | path: src/Protocol/SchemaValidationFailure.php 22 | 23 | - 24 | message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" 25 | count: 3 26 | path: src/Protocol/Type/ArrayOf.php 27 | 28 | - 29 | message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#" 30 | count: 1 31 | path: src/Protocol/Type/ArrayOf.php 32 | 33 | - 34 | message: "#^Parameter \\#1 \\$data of method Lcobucci\\\\Kafka\\\\Protocol\\\\Type\\:\\:guardRange\\(\\) expects int, mixed given\\.$#" 35 | count: 1 36 | path: src/Protocol/Type/Int16.php 37 | 38 | - 39 | message: "#^Parameter \\#1 \\$value of method Lcobucci\\\\Kafka\\\\Protocol\\\\Buffer\\:\\:writeShort\\(\\) expects int, mixed given\\.$#" 40 | count: 1 41 | path: src/Protocol/Type/Int16.php 42 | 43 | - 44 | message: "#^Parameter \\#1 \\$data of method Lcobucci\\\\Kafka\\\\Protocol\\\\Type\\:\\:guardRange\\(\\) expects int, mixed given\\.$#" 45 | count: 1 46 | path: src/Protocol/Type/Int32.php 47 | 48 | - 49 | message: "#^Parameter \\#1 \\$value of method Lcobucci\\\\Kafka\\\\Protocol\\\\Buffer\\:\\:writeInt\\(\\) expects int, mixed given\\.$#" 50 | count: 1 51 | path: src/Protocol/Type/Int32.php 52 | 53 | - 54 | message: "#^Parameter \\#1 \\$value of method Lcobucci\\\\Kafka\\\\Protocol\\\\Buffer\\:\\:writeLong\\(\\) expects int, mixed given\\.$#" 55 | count: 1 56 | path: src/Protocol/Type/Int64.php 57 | 58 | - 59 | message: "#^Parameter \\#1 \\$data of method Lcobucci\\\\Kafka\\\\Protocol\\\\Type\\:\\:guardRange\\(\\) expects int, mixed given\\.$#" 60 | count: 1 61 | path: src/Protocol/Type/Int8.php 62 | 63 | - 64 | message: "#^Parameter \\#1 \\$value of method Lcobucci\\\\Kafka\\\\Protocol\\\\Buffer\\:\\:writeByte\\(\\) expects int, mixed given\\.$#" 65 | count: 1 66 | path: src/Protocol/Type/Int8.php 67 | 68 | - 69 | message: "#^Parameter \\#1 \\$content of static method Lcobucci\\\\Kafka\\\\Protocol\\\\Buffer\\:\\:fromContent\\(\\) expects string, mixed given\\.$#" 70 | count: 1 71 | path: src/Protocol/Type/NonNullableBytes.php 72 | 73 | - 74 | message: "#^Parameter \\#1 \\$data of method Lcobucci\\\\Kafka\\\\Protocol\\\\Type\\:\\:guardClass\\(\\) expects object\\|null, mixed given\\.$#" 75 | count: 1 76 | path: src/Protocol/Type/NonNullableBytes.php 77 | 78 | - 79 | message: "#^Parameter \\#1 \\$object_or_class of function is_a expects object, mixed given\\.$#" 80 | count: 2 81 | path: src/Protocol/Type/NonNullableBytes.php 82 | 83 | - 84 | message: "#^Method Lcobucci\\\\Kafka\\\\Protocol\\\\Type\\\\NonNullableString\\:\\:read\\(\\) should return string but returns mixed\\.$#" 85 | count: 1 86 | path: src/Protocol/Type/NonNullableString.php 87 | 88 | - 89 | message: "#^Parameter \\#1 \\$data of method Lcobucci\\\\Kafka\\\\Protocol\\\\Type\\:\\:guardLength\\(\\) expects string\\|null, mixed given\\.$#" 90 | count: 1 91 | path: src/Protocol/Type/NonNullableString.php 92 | 93 | - 94 | message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#" 95 | count: 2 96 | path: src/Protocol/Type/NonNullableString.php 97 | 98 | - 99 | message: "#^Parameter \\#1 \\$value of method Lcobucci\\\\Kafka\\\\Protocol\\\\Buffer\\:\\:write\\(\\) expects string, mixed given\\.$#" 100 | count: 1 101 | path: src/Protocol/Type/NonNullableString.php 102 | 103 | - 104 | message: "#^Parameter \\#1 \\$content of static method Lcobucci\\\\Kafka\\\\Protocol\\\\Buffer\\:\\:fromContent\\(\\) expects string, mixed given\\.$#" 105 | count: 1 106 | path: src/Protocol/Type/NullableBytes.php 107 | 108 | - 109 | message: "#^Parameter \\#1 \\$data of method Lcobucci\\\\Kafka\\\\Protocol\\\\Type\\:\\:guardClass\\(\\) expects object\\|null, mixed given\\.$#" 110 | count: 1 111 | path: src/Protocol/Type/NullableBytes.php 112 | 113 | - 114 | message: "#^Method Lcobucci\\\\Kafka\\\\Protocol\\\\Type\\\\NullableString\\:\\:read\\(\\) should return string\\|null but returns mixed\\.$#" 115 | count: 1 116 | path: src/Protocol/Type/NullableString.php 117 | 118 | - 119 | message: "#^Parameter \\#1 \\$data of method Lcobucci\\\\Kafka\\\\Protocol\\\\Type\\:\\:guardLength\\(\\) expects string\\|null, mixed given\\.$#" 120 | count: 1 121 | path: src/Protocol/Type/NullableString.php 122 | 123 | - 124 | message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#" 125 | count: 2 126 | path: src/Protocol/Type/NullableString.php 127 | 128 | - 129 | message: "#^Parameter \\#1 \\$value of method Lcobucci\\\\Kafka\\\\Protocol\\\\Buffer\\:\\:write\\(\\) expects string, mixed given\\.$#" 130 | count: 1 131 | path: src/Protocol/Type/NullableString.php 132 | 133 | - 134 | message: "#^Parameter \\#1 \\$data of method Lcobucci\\\\Kafka\\\\Protocol\\\\Type\\:\\:guardRange\\(\\) expects int, mixed given\\.$#" 135 | count: 1 136 | path: src/Protocol/Type/UnsignedInt32.php 137 | 138 | - 139 | message: "#^Parameter \\#1 \\$value of method Lcobucci\\\\Kafka\\\\Protocol\\\\Buffer\\:\\:writeUnsignedInt\\(\\) expects int, mixed given\\.$#" 140 | count: 1 141 | path: src/Protocol/Type/UnsignedInt32.php 142 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>lcobucci/.github:renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | bytes; 58 | } 59 | 60 | /** 61 | * Returns the length of allocated bytes 62 | */ 63 | public function length(): int 64 | { 65 | return $this->length; 66 | } 67 | 68 | /** 69 | * Resets the position to its initial state (for reading/writing) 70 | */ 71 | public function reset(): void 72 | { 73 | $this->position = 0; 74 | } 75 | 76 | /** 77 | * Returns the remaining allocated bytes (based on the current position) 78 | */ 79 | public function remaining(): int 80 | { 81 | return $this->length - $this->position; 82 | } 83 | 84 | /** 85 | * Returns the current position 86 | */ 87 | public function position(): int 88 | { 89 | return $this->position; 90 | } 91 | 92 | /** 93 | * Returns N bytes from an offset without modifying the current position 94 | * 95 | * @throws NotEnoughBytesAllocated When trying to read from an invalid offset. 96 | */ 97 | public function get(int $offset, int $length = 1): string 98 | { 99 | if ($this->length - $offset < $length) { 100 | throw NotEnoughBytesAllocated::forLength($length); 101 | } 102 | 103 | return substr($this->bytes, $offset, $length); 104 | } 105 | 106 | /** 107 | * Returns the current offset to start reading/writing and moves the cursor by given length 108 | * 109 | * @throws NotEnoughBytesAllocated When trying to read/write from/into an invalid position. 110 | */ 111 | private function nextIndex(int $length): int 112 | { 113 | if ($this->remaining() < $length) { 114 | throw NotEnoughBytesAllocated::forLength($length); 115 | } 116 | 117 | $currentPosition = $this->position; 118 | $this->position += $length; 119 | 120 | return $currentPosition; 121 | } 122 | 123 | /** 124 | * Overwrites allocated bytes in the current position 125 | * 126 | * @throws NotEnoughBytesAllocated When trying to write into an invalid position. 127 | */ 128 | public function write(string $value): void 129 | { 130 | $length = strlen($value); 131 | $offset = $this->nextIndex($length); 132 | 133 | $this->bytes = substr_replace($this->bytes, $value, $offset, $length); 134 | } 135 | 136 | /** 137 | * Reads an amount of bytes from the current position 138 | * 139 | * @throws NotEnoughBytesAllocated When trying to read from an invalid position. 140 | */ 141 | public function read(int $length): mixed 142 | { 143 | $offset = $this->nextIndex($length); 144 | 145 | return substr($this->bytes, $offset, $length); 146 | } 147 | 148 | /** 149 | * Writes a single numeric byte (from -128 to 127) 150 | * 151 | * @throws NotEnoughBytesAllocated When trying to write into an invalid position. 152 | * @throws ValueOutOfAllowedRange When value is not between a signed byte range (-2^7, 2^7 - 1). 153 | */ 154 | public function writeByte(int $value): void 155 | { 156 | $this->guardBounds($value, ...self::BYTE_RANGE); 157 | $this->write(pack('c', $value)); 158 | } 159 | 160 | /** 161 | * Validates if given value is between given bounds (inclusive) 162 | * 163 | * @throws ValueOutOfAllowedRange When value is not between the given range. 164 | */ 165 | private function guardBounds(int $value, int $lowerBound, int $upperBound): void 166 | { 167 | if ($value < $lowerBound || $value > $upperBound) { 168 | throw ValueOutOfAllowedRange::forRange($value, $lowerBound, $upperBound); 169 | } 170 | } 171 | 172 | /** 173 | * Reads a single numeric byte (from -128 to 127) 174 | * 175 | * @throws NotEnoughBytesAllocated When trying to read from an invalid position. 176 | */ 177 | public function readByte(): int 178 | { 179 | return $this->unpack('c', $this->read(1)); 180 | } 181 | 182 | /** 183 | * Writes a short number (from -32768 to 32767) 184 | * 185 | * @throws NotEnoughBytesAllocated When trying to write into an invalid position. 186 | * @throws ValueOutOfAllowedRange When value is not between a signed short range (-2^15, 2^15 - 1). 187 | */ 188 | public function writeShort(int $value): void 189 | { 190 | $this->guardBounds($value, ...self::SHORT_RANGE); 191 | $this->write(pack('n', $value)); 192 | } 193 | 194 | /** 195 | * Reads a short number (from -32768 to 32767) 196 | * 197 | * @throws NotEnoughBytesAllocated When trying to read from an invalid position. 198 | */ 199 | public function readShort(): int 200 | { 201 | return $this->convertToSigned($this->unpack('n', $this->read(2)), ...self::CONVERSION_SHORT); 202 | } 203 | 204 | /** 205 | * Converts an unsigned number into a signed one based on given values 206 | */ 207 | private function convertToSigned(int $value, int $maxValue, int $subtract): int 208 | { 209 | if ($value <= $maxValue) { 210 | return $value; 211 | } 212 | 213 | return $value - $subtract; 214 | } 215 | 216 | /** 217 | * Writes a 32-bit integer (from -2147483648 to 2147483647) 218 | * 219 | * @throws NotEnoughBytesAllocated When trying to write into an invalid position. 220 | * @throws ValueOutOfAllowedRange When value is not between a signed integer range (-2^31, 2^31 - 1). 221 | */ 222 | public function writeInt(int $value): void 223 | { 224 | $this->guardBounds($value, ...self::INT_RANGE); 225 | $this->write(pack('N', $value)); 226 | } 227 | 228 | /** 229 | * Reads a 32-bit integer (from -2147483648 to 2147483647) 230 | * 231 | * @throws NotEnoughBytesAllocated When trying to read from an invalid position. 232 | */ 233 | public function readInt(): int 234 | { 235 | return $this->convertToSigned($this->unpack('N', $this->read(4)), ...self::CONVERSION_INT); 236 | } 237 | 238 | /** 239 | * Writes an unsigned 32-bit integer (from 0 to 4294967295) 240 | * 241 | * @throws NotEnoughBytesAllocated When trying to write into an invalid position. 242 | * @throws ValueOutOfAllowedRange When value is not between an unsigned integer range (0, 2^32 - 1). 243 | */ 244 | public function writeUnsignedInt(int $value): void 245 | { 246 | $this->guardBounds($value, ...self::UINT_RANGE); 247 | $this->write(pack('N', $value)); 248 | } 249 | 250 | /** 251 | * Reads an unsigned 32-bit integer (from 0 to 4294967295) 252 | * 253 | * @throws NotEnoughBytesAllocated When trying to read from an invalid position. 254 | */ 255 | public function readUnsignedInt(): int 256 | { 257 | return $this->unpack('N', $this->read(4)); 258 | } 259 | 260 | /** 261 | * Writes a 64-bit integer (from -9223372036854775808 to 9223372036854775807) 262 | * 263 | * Can't throw \Lcobucci\Kafka\Protocol\ValueOutOfAllowedRange due to type declarations (PHP will convert the number 264 | * to a double if it's out of bounds, and strict types will force an error). 265 | * 266 | * @throws NotEnoughBytesAllocated When trying to write into an invalid position. 267 | */ 268 | public function writeLong(int $value): void 269 | { 270 | $this->write(pack('J', $value)); 271 | } 272 | 273 | /** 274 | * Reads a 64-bit integer (from -9223372036854775808 to 9223372036854775807) 275 | * 276 | * Doesn't need to be converted to signed since PHP doesn't support unsigned 64-bit integers. 277 | * 278 | * @throws NotEnoughBytesAllocated When trying to read from an invalid position. 279 | */ 280 | public function readLong(): int 281 | { 282 | return $this->unpack('J', $this->read(8)); 283 | } 284 | 285 | private function unpack(string $format, string $data): int 286 | { 287 | $unpacked = unpack($format, $data); 288 | assert(is_array($unpacked)); 289 | 290 | return $unpacked[1]; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/Protocol/NotEnoughBytesAllocated.php: -------------------------------------------------------------------------------- 1 | */ 17 | private array $fields; 18 | 19 | public function __construct(Field ...$fields) 20 | { 21 | $this->fields = $fields; 22 | } 23 | 24 | public function write(mixed $data, Buffer $buffer): void 25 | { 26 | assert(is_array($data)); 27 | 28 | foreach ($this->fields as $field) { 29 | $field->writeTo($data, $buffer); 30 | } 31 | } 32 | 33 | /** 34 | * @return array 35 | * 36 | * @inheritdoc 37 | */ 38 | public function read(Buffer $buffer): array 39 | { 40 | $structure = []; 41 | 42 | foreach ($this->fields as $field) { 43 | $structure[$field->name()] = $field->readFrom($buffer); 44 | } 45 | 46 | return $structure; 47 | } 48 | 49 | public function sizeOf(mixed $data): int 50 | { 51 | assert(is_array($data)); 52 | 53 | $size = 0; 54 | 55 | foreach ($this->fields as $field) { 56 | $size += $field->sizeOf($data); 57 | } 58 | 59 | return $size; 60 | } 61 | 62 | public function validate(mixed $data): void 63 | { 64 | $this->guardAgainstNull($data, 'array'); 65 | $this->guardType($data, 'array', 'is_array'); 66 | 67 | foreach ($this->fields as $field) { 68 | $this->validateField($data, $field); 69 | } 70 | } 71 | 72 | /** 73 | * @param array $data 74 | * 75 | * @throws SchemaValidationFailure When value is not valid for given field. 76 | */ 77 | private function validateField(array $data, Field $field): void 78 | { 79 | try { 80 | $field->validate($data); 81 | } catch (SchemaValidationFailure $failure) { 82 | throw SchemaValidationFailure::invalidValueForField($field->name(), $failure); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Protocol/Schema/Field.php: -------------------------------------------------------------------------------- 1 | name; 23 | } 24 | 25 | /** 26 | * Writes content to the message using field type 27 | * 28 | * @param mixed[] $structure 29 | * 30 | * @throws SchemaValidationFailure When field is not nullable and missing from the structure. 31 | * @throws NotEnoughBytesAllocated When size of given value is bigger than the remaining allocated bytes. 32 | */ 33 | public function writeTo(array $structure, Buffer $buffer): void 34 | { 35 | $this->type->write($this->extractValue($structure), $buffer); 36 | } 37 | 38 | /** 39 | * Reads content from message using field type 40 | * 41 | * @throws NotEnoughBytesAllocated When trying to read a content bigger than the remaining allocated bytes. 42 | */ 43 | public function readFrom(Buffer $buffer): mixed 44 | { 45 | return $this->type->read($buffer); 46 | } 47 | 48 | /** 49 | * Returns the number of bytes necessary for this field 50 | * 51 | * @param mixed[] $structure 52 | * 53 | * @throws SchemaValidationFailure When field is not nullable and missing from the structure. 54 | */ 55 | public function sizeOf(array $structure): int 56 | { 57 | return $this->type->sizeOf($this->extractValue($structure)); 58 | } 59 | 60 | /** 61 | * Ensures that given data is valid 62 | * 63 | * @param array $structure 64 | * 65 | * @throws SchemaValidationFailure When field is not nullable and missing from the structure, or data is invalid. 66 | */ 67 | public function validate(array $structure): void 68 | { 69 | $this->type->validate($this->extractValue($structure)); 70 | } 71 | 72 | /** 73 | * Returns the value for the field, falling back to null (when possible) 74 | * 75 | * @param array $structure 76 | * 77 | * @throws SchemaValidationFailure When field is not nullable and missing from the structure. 78 | */ 79 | private function extractValue(array $structure): mixed 80 | { 81 | if (! isset($structure[$this->name]) && ! $this->type->isNullable()) { 82 | throw SchemaValidationFailure::missingField($this->name); 83 | } 84 | 85 | return $structure[$this->name] ?? null; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Protocol/SchemaValidationFailure.php: -------------------------------------------------------------------------------- 1 | getMessage()), 61 | previous: $failure, 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Protocol/Type.php: -------------------------------------------------------------------------------- 1 | $upperBound) { 95 | throw SchemaValidationFailure::incorrectRange($data, $lowerBound, $upperBound); 96 | } 97 | } 98 | 99 | /** 100 | * Ensures that given value's length is not larger than expected maximum length 101 | * 102 | * @throws SchemaValidationFailure 103 | */ 104 | final protected function guardLength(?string $data, int $maxLength): void 105 | { 106 | if ($data === null) { 107 | return; 108 | } 109 | 110 | $length = strlen($data); 111 | 112 | if ($length > $maxLength) { 113 | throw SchemaValidationFailure::incorrectLength($length, $maxLength); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Protocol/Type/ArrayOf.php: -------------------------------------------------------------------------------- 1 | writeInt(-1); 21 | 22 | return; 23 | } 24 | 25 | $buffer->writeInt(count($data)); 26 | 27 | foreach ($data as $item) { 28 | $this->type->write($item, $buffer); 29 | } 30 | } 31 | 32 | /** 33 | * @return list|null 34 | * 35 | * @inheritdoc 36 | */ 37 | public function read(Buffer $buffer): ?array 38 | { 39 | $count = $buffer->readInt(); 40 | 41 | if ($count < 0) { 42 | return null; 43 | } 44 | 45 | $items = []; 46 | 47 | for ($i = 0; $i < $count; ++$i) { 48 | $items[] = $this->type->read($buffer); 49 | } 50 | 51 | return $items; 52 | } 53 | 54 | public function sizeOf(mixed $data): int 55 | { 56 | if ($data === null) { 57 | return 4; 58 | } 59 | 60 | $size = 4; 61 | 62 | foreach ($data as $item) { 63 | $size += $this->type->sizeOf($item); 64 | } 65 | 66 | return $size; 67 | } 68 | 69 | public function isNullable(): bool 70 | { 71 | return $this->nullable; 72 | } 73 | 74 | public function validate(mixed $data): void 75 | { 76 | if (! $this->nullable) { 77 | $this->guardAgainstNull($data, 'array'); 78 | } 79 | 80 | if ($data === null) { 81 | return; 82 | } 83 | 84 | $this->guardType($data, 'array', 'is_array'); 85 | 86 | foreach ($data as $item) { 87 | $this->type->validate($item); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Protocol/Type/Boolean.php: -------------------------------------------------------------------------------- 1 | writeByte($data === true ? 1 : 0); 20 | } 21 | 22 | public function read(Buffer $buffer): bool 23 | { 24 | return $buffer->readByte() !== 0; 25 | } 26 | 27 | public function sizeOf(mixed $data): int // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter 28 | { 29 | return 1; 30 | } 31 | 32 | public function validate(mixed $data): void 33 | { 34 | $this->guardAgainstNull($data, 'boolean'); 35 | $this->guardType($data, 'boolean', 'is_bool'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Protocol/Type/Int16.php: -------------------------------------------------------------------------------- 1 | writeShort($data); 20 | } 21 | 22 | public function read(Buffer $buffer): int 23 | { 24 | return $buffer->readShort(); 25 | } 26 | 27 | public function sizeOf(mixed $data): int // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter 28 | { 29 | return 2; 30 | } 31 | 32 | public function validate(mixed $data): void 33 | { 34 | $this->guardAgainstNull($data, 'integer'); 35 | $this->guardType($data, 'integer', 'is_int'); 36 | $this->guardRange($data, self::MIN, self::MAX); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Protocol/Type/Int32.php: -------------------------------------------------------------------------------- 1 | writeInt($data); 20 | } 21 | 22 | public function read(Buffer $buffer): int 23 | { 24 | return $buffer->readInt(); 25 | } 26 | 27 | public function sizeOf(mixed $data): int // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter 28 | { 29 | return 4; 30 | } 31 | 32 | public function validate(mixed $data): void 33 | { 34 | $this->guardAgainstNull($data, 'integer'); 35 | $this->guardType($data, 'integer', 'is_int'); 36 | $this->guardRange($data, self::MIN, self::MAX); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Protocol/Type/Int64.php: -------------------------------------------------------------------------------- 1 | writeLong($data); 17 | } 18 | 19 | public function read(Buffer $buffer): int 20 | { 21 | return $buffer->readLong(); 22 | } 23 | 24 | public function sizeOf(mixed $data): int // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter 25 | { 26 | return 8; 27 | } 28 | 29 | public function validate(mixed $data): void 30 | { 31 | $this->guardAgainstNull($data, 'integer'); 32 | $this->guardType($data, 'integer', 'is_int'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Protocol/Type/Int8.php: -------------------------------------------------------------------------------- 1 | writeByte($data); 20 | } 21 | 22 | public function read(Buffer $buffer): int 23 | { 24 | return $buffer->readByte(); 25 | } 26 | 27 | public function sizeOf(mixed $data): int // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter 28 | { 29 | return 1; 30 | } 31 | 32 | public function validate(mixed $data): void 33 | { 34 | $this->guardAgainstNull($data, 'integer'); 35 | $this->guardType($data, 'integer', 'is_int'); 36 | $this->guardRange($data, self::MIN, self::MAX); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Protocol/Type/NonNullableBytes.php: -------------------------------------------------------------------------------- 1 | remaining(); 24 | $position = $data->position(); 25 | 26 | $buffer->writeInt($length); 27 | $buffer->write($data->get($position, $length)); 28 | } 29 | 30 | public function read(Buffer $buffer): Buffer 31 | { 32 | return Buffer::fromContent( 33 | $buffer->read($buffer->readInt()), 34 | ); 35 | } 36 | 37 | public function sizeOf(mixed $data): int 38 | { 39 | assert(is_a($data, Buffer::class)); 40 | 41 | return 4 + $data->remaining(); 42 | } 43 | 44 | public function validate(mixed $data): void 45 | { 46 | $this->guardAgainstNull($data, Buffer::class); 47 | $this->guardType($data, 'object', 'is_object'); 48 | $this->guardClass($data, Buffer::class); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Protocol/Type/NonNullableString.php: -------------------------------------------------------------------------------- 1 | writeShort(strlen($data)); 24 | $buffer->write($data); 25 | } 26 | 27 | public function read(Buffer $buffer): string 28 | { 29 | return $buffer->read($buffer->readShort()); 30 | } 31 | 32 | public function sizeOf(mixed $data): int 33 | { 34 | return 2 + strlen($data); 35 | } 36 | 37 | public function validate(mixed $data): void 38 | { 39 | $this->guardAgainstNull($data, 'string'); 40 | $this->guardType($data, 'string', 'is_string'); 41 | $this->guardLength($data, self::MAX_LENGTH); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Protocol/Type/NullableBytes.php: -------------------------------------------------------------------------------- 1 | writeInt(-1); 22 | 23 | return; 24 | } 25 | 26 | $length = $data->remaining(); 27 | $position = $data->position(); 28 | 29 | $buffer->writeInt($length); 30 | $buffer->write($data->get($position, $length)); 31 | } 32 | 33 | public function read(Buffer $buffer): ?Buffer 34 | { 35 | $length = $buffer->readInt(); 36 | 37 | if ($length < 0) { 38 | return null; 39 | } 40 | 41 | return Buffer::fromContent($buffer->read($length)); 42 | } 43 | 44 | public function sizeOf(mixed $data): int 45 | { 46 | if (! $data instanceof Buffer) { 47 | return 4; 48 | } 49 | 50 | return 4 + $data->remaining(); 51 | } 52 | 53 | public function isNullable(): bool 54 | { 55 | return true; 56 | } 57 | 58 | public function validate(mixed $data): void 59 | { 60 | $this->guardType($data, 'object', 'is_object'); 61 | $this->guardClass($data, Buffer::class); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Protocol/Type/NullableString.php: -------------------------------------------------------------------------------- 1 | writeShort(-1); 26 | 27 | return; 28 | } 29 | 30 | $buffer->writeShort(strlen($data)); 31 | $buffer->write($data); 32 | } 33 | 34 | public function read(Buffer $buffer): ?string 35 | { 36 | $length = $buffer->readShort(); 37 | 38 | if ($length < 0) { 39 | return null; 40 | } 41 | 42 | return $buffer->read($length); 43 | } 44 | 45 | public function sizeOf(mixed $data): int 46 | { 47 | if ($data === null) { 48 | return 2; 49 | } 50 | 51 | return 2 + strlen($data); 52 | } 53 | 54 | public function isNullable(): bool 55 | { 56 | return true; 57 | } 58 | 59 | public function validate(mixed $data): void 60 | { 61 | $this->guardType($data, 'string', 'is_string'); 62 | $this->guardLength($data, self::MAX_LENGTH); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Protocol/Type/UnsignedInt32.php: -------------------------------------------------------------------------------- 1 | writeUnsignedInt($data); 20 | } 21 | 22 | public function read(Buffer $buffer): int 23 | { 24 | return $buffer->readUnsignedInt(); 25 | } 26 | 27 | public function sizeOf(mixed $data): int // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter 28 | { 29 | return 4; 30 | } 31 | 32 | public function validate(mixed $data): void 33 | { 34 | $this->guardAgainstNull($data, 'integer'); 35 | $this->guardType($data, 'integer', 'is_int'); 36 | $this->guardRange($data, self::MIN, self::MAX); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Protocol/ValueOutOfAllowedRange.php: -------------------------------------------------------------------------------- 1 |