├── RELEASING.md ├── .gitignore ├── MAINTAINERS.md ├── src ├── CloudEventInterface.php ├── Serializers │ ├── Normalizers │ │ └── V1 │ │ │ ├── NormalizerInterface.php │ │ │ ├── DenormalizerInterface.php │ │ │ ├── Normalizer.php │ │ │ └── Denormalizer.php │ ├── SerializerInterface.php │ ├── DeserializerInterface.php │ ├── JsonSerializer.php │ └── JsonDeserializer.php ├── Exceptions │ ├── InvalidAttributeException.php │ ├── MissingAttributeException.php │ ├── InvalidPayloadSyntaxException.php │ ├── UnsupportedContentTypeException.php │ └── UnsupportedSpecVersionException.php ├── V1 │ ├── CloudEvent.php │ ├── CloudEventInterface.php │ ├── CloudEventImmutable.php │ └── CloudEventTrait.php ├── Http │ ├── UnmarshallerInterface.php │ ├── MarshallerInterface.php │ ├── Unmarshaller.php │ └── Marshaller.php └── Utilities │ ├── DataFormatter.php │ ├── TimeFormatter.php │ └── AttributeConverter.php ├── psalm-baseline.xml ├── hack ├── install-composer ├── 8.0.Dockerfile ├── 8.1.Dockerfile ├── 8.2.Dockerfile ├── 8.3.Dockerfile ├── 8.4.Dockerfile └── 7.4.Dockerfile ├── psalm.xml.dist ├── phpunit.xml.dist ├── .github └── workflows │ ├── tests.yml │ └── static.yml ├── tests └── Unit │ ├── Utilities │ ├── DataFormatterTest.php │ └── TimeFormatterTest.php │ ├── Serializers │ ├── Normalizers │ │ └── V1 │ │ │ ├── DenormalizerTest.php │ │ │ └── NormalizerTest.php │ ├── JsonSerializerTest.php │ └── JsonDeserializerTest.php │ ├── V1 │ ├── CloudEventTest.php │ └── CloudEventImmutableTest.php │ └── Http │ ├── MarshallerTest.php │ └── UnmarshallerTest.php ├── composer.json ├── README.md ├── CONTRIBUTING.md ├── LICENSE └── phpcs.xml.dist /RELEASING.md: -------------------------------------------------------------------------------- 1 | To create a new release: 2 | - Create a new Github release via the Github UI, making sure to prefix it 3 | with `v` 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .phpunit.cache 2 | .phpunit.result.cache 3 | 4 | composer.lock 5 | phpcs.xml 6 | phpunit.xml 7 | psalm.xml 8 | 9 | coverage 10 | vendor 11 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | Current active maintainers of this SDK: 4 | 5 | - [John Laswell](https://github.com/jlaswell) 6 | - [Graham Campbell](https://github.com/grahamcampbell) 7 | -------------------------------------------------------------------------------- /src/CloudEventInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function normalize(CloudEventInterface $cloudEvent, bool $rawData): array; 15 | } 16 | -------------------------------------------------------------------------------- /psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /hack/install-composer: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" 4 | php -r "if (hash_file('sha384', 'composer-setup.php') === '756890a4488ce9024fc62c56153228907f1545c228516cbf63f885e036d37e9a59d27d63f46af1d4d07ee0f76181c7d3') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \ 5 | php composer-setup.php --install-dir=/usr/local/bin --filename=composer --quiet 6 | php -r "unlink('composer-setup.php');" \ 7 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidAttributeException.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/V1/CloudEvent.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests/Unit 6 | 7 | 8 | 9 | 10 | src 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Serializers/Normalizers/V1/DenormalizerInterface.php: -------------------------------------------------------------------------------- 1 | $payload 16 | * 17 | * @throws InvalidAttributeException 18 | * @throws UnsupportedSpecVersionException 19 | * @throws MissingAttributeException 20 | */ 21 | public function denormalize(array $payload): CloudEventInterface; 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/UnmarshallerInterface.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function unmarshal(MessageInterface $message): array; 25 | } 26 | -------------------------------------------------------------------------------- /src/Serializers/SerializerInterface.php: -------------------------------------------------------------------------------- 1 | $cloudEvents 19 | * 20 | * @throws UnsupportedSpecVersionException 21 | */ 22 | public function serializeBatch(array $cloudEvents): string; 23 | 24 | /** 25 | * @throws UnsupportedSpecVersionException 26 | * 27 | * @return array{data: string, contentType: string, attributes: array} 28 | */ 29 | public function serializeBinary(CloudEventInterface $cloudEvent): array; 30 | } 31 | -------------------------------------------------------------------------------- /src/V1/CloudEventInterface.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function getExtensions(): array; 36 | 37 | /** 38 | * @return bool|int|string|null 39 | */ 40 | public function getExtension(string $attribute); 41 | } 42 | -------------------------------------------------------------------------------- /hack/8.0.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0-alpine 2 | 3 | LABEL org.opencontainers.image.url="https://github.com/cloudevents/sdk-php/tree/main/hack/8.0.Dockerfile" \ 4 | org.opencontainers.image.documentation="https://github.com/cloudevents/sdk-php/tree/main/hack/README.md" \ 5 | org.opencontainers.image.source="https://github.com/cloudevents/sdk-php" \ 6 | org.opencontainers.image.vendor="CloudEvent" \ 7 | org.opencontainers.image.title="PHP 8.0" \ 8 | org.opencontainers.image.description="PHP 8.0 test environment for cloudevents/sdk-php" 9 | 10 | COPY --chown=www-data:www-data install-composer /usr/local/bin/install-composer 11 | RUN chmod +x /usr/local/bin/install-composer \ 12 | && /usr/local/bin/install-composer \ 13 | && rm /usr/local/bin/install-composer 14 | 15 | RUN apk update \ 16 | && apk --no-cache upgrade \ 17 | && apk add --no-cache bash ca-certificates git libzip-dev \ 18 | && rm -rf /var/www/html /tmp/pear \ 19 | && chown -R www-data:www-data /var/www 20 | 21 | WORKDIR /var/www 22 | ENTRYPOINT ["/var/www/vendor/bin/phpunit"] 23 | -------------------------------------------------------------------------------- /hack/8.1.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-alpine 2 | 3 | LABEL org.opencontainers.image.url="https://github.com/cloudevents/sdk-php/tree/main/hack/8.1.Dockerfile" \ 4 | org.opencontainers.image.documentation="https://github.com/cloudevents/sdk-php/tree/main/hack/README.md" \ 5 | org.opencontainers.image.source="https://github.com/cloudevents/sdk-php" \ 6 | org.opencontainers.image.vendor="CloudEvent" \ 7 | org.opencontainers.image.title="PHP 8.1" \ 8 | org.opencontainers.image.description="PHP 8.1 test environment for cloudevents/sdk-php" 9 | 10 | COPY --chown=www-data:www-data install-composer /usr/local/bin/install-composer 11 | RUN chmod +x /usr/local/bin/install-composer \ 12 | && /usr/local/bin/install-composer \ 13 | && rm /usr/local/bin/install-composer 14 | 15 | RUN apk update \ 16 | && apk --no-cache upgrade \ 17 | && apk add --no-cache bash ca-certificates git libzip-dev \ 18 | && rm -rf /var/www/html /tmp/pear \ 19 | && chown -R www-data:www-data /var/www 20 | 21 | WORKDIR /var/www 22 | ENTRYPOINT ["/var/www/vendor/bin/phpunit"] 23 | -------------------------------------------------------------------------------- /hack/8.2.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.2-alpine 2 | 3 | LABEL org.opencontainers.image.url="https://github.com/cloudevents/sdk-php/tree/main/hack/8.2.Dockerfile" \ 4 | org.opencontainers.image.documentation="https://github.com/cloudevents/sdk-php/tree/main/hack/README.md" \ 5 | org.opencontainers.image.source="https://github.com/cloudevents/sdk-php" \ 6 | org.opencontainers.image.vendor="CloudEvent" \ 7 | org.opencontainers.image.title="PHP 8.2" \ 8 | org.opencontainers.image.description="PHP 8.2 test environment for cloudevents/sdk-php" 9 | 10 | COPY --chown=www-data:www-data install-composer /usr/local/bin/install-composer 11 | RUN chmod +x /usr/local/bin/install-composer \ 12 | && /usr/local/bin/install-composer \ 13 | && rm /usr/local/bin/install-composer 14 | 15 | RUN apk update \ 16 | && apk --no-cache upgrade \ 17 | && apk add --no-cache bash ca-certificates git libzip-dev \ 18 | && rm -rf /var/www/html /tmp/pear \ 19 | && chown -R www-data:www-data /var/www 20 | 21 | WORKDIR /var/www 22 | ENTRYPOINT ["/var/www/vendor/bin/phpunit"] 23 | -------------------------------------------------------------------------------- /hack/8.3.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.3-alpine 2 | 3 | LABEL org.opencontainers.image.url="https://github.com/cloudevents/sdk-php/tree/main/hack/8.3.Dockerfile" \ 4 | org.opencontainers.image.documentation="https://github.com/cloudevents/sdk-php/tree/main/hack/README.md" \ 5 | org.opencontainers.image.source="https://github.com/cloudevents/sdk-php" \ 6 | org.opencontainers.image.vendor="CloudEvent" \ 7 | org.opencontainers.image.title="PHP 8.3" \ 8 | org.opencontainers.image.description="PHP 8.3 test environment for cloudevents/sdk-php" 9 | 10 | COPY --chown=www-data:www-data install-composer /usr/local/bin/install-composer 11 | RUN chmod +x /usr/local/bin/install-composer \ 12 | && /usr/local/bin/install-composer \ 13 | && rm /usr/local/bin/install-composer 14 | 15 | RUN apk update \ 16 | && apk --no-cache upgrade \ 17 | && apk add --no-cache bash ca-certificates git libzip-dev \ 18 | && rm -rf /var/www/html /tmp/pear \ 19 | && chown -R www-data:www-data /var/www 20 | 21 | WORKDIR /var/www 22 | ENTRYPOINT ["/var/www/vendor/bin/phpunit"] 23 | -------------------------------------------------------------------------------- /hack/8.4.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.4-alpine 2 | 3 | LABEL org.opencontainers.image.url="https://github.com/cloudevents/sdk-php/tree/main/hack/8.4.Dockerfile" \ 4 | org.opencontainers.image.documentation="https://github.com/cloudevents/sdk-php/tree/main/hack/README.md" \ 5 | org.opencontainers.image.source="https://github.com/cloudevents/sdk-php" \ 6 | org.opencontainers.image.vendor="CloudEvent" \ 7 | org.opencontainers.image.title="PHP 8.4" \ 8 | org.opencontainers.image.description="PHP 8.4 test environment for cloudevents/sdk-php" 9 | 10 | COPY --chown=www-data:www-data install-composer /usr/local/bin/install-composer 11 | RUN chmod +x /usr/local/bin/install-composer \ 12 | && /usr/local/bin/install-composer \ 13 | && rm /usr/local/bin/install-composer 14 | 15 | RUN apk update \ 16 | && apk --no-cache upgrade \ 17 | && apk add --no-cache bash ca-certificates git libzip-dev \ 18 | && rm -rf /var/www/html /tmp/pear \ 19 | && chown -R www-data:www-data /var/www 20 | 21 | WORKDIR /var/www 22 | ENTRYPOINT ["/var/www/vendor/bin/phpunit"] 23 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | tests: 10 | name: PHP ${{ matrix.php }} 11 | runs-on: ubuntu-24.04 12 | 13 | strategy: 14 | matrix: 15 | php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] 16 | 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@v3 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | tools: composer:v2 26 | coverage: none 27 | env: 28 | update: true 29 | 30 | - name: Setup Problem Matchers 31 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 32 | 33 | - name: Install Dependencies 34 | uses: nick-invision/retry@v3 35 | with: 36 | timeout_minutes: 5 37 | max_attempts: 5 38 | command: composer update --no-interaction --no-progress 39 | 40 | - name: Execute PHPUnit 41 | run: vendor/bin/phpunit 42 | -------------------------------------------------------------------------------- /src/Serializers/Normalizers/V1/Normalizer.php: -------------------------------------------------------------------------------- 1 | } 15 | */ 16 | private array $configuration; 17 | 18 | /** 19 | * @param array{subsecondPrecision?: int<0, 6>} $configuration 20 | */ 21 | public function __construct(array $configuration = []) 22 | { 23 | $this->configuration = $configuration; 24 | } 25 | 26 | /** 27 | * @return array 28 | */ 29 | public function normalize(CloudEventInterface $cloudEvent, bool $rawData): array 30 | { 31 | return array_merge( 32 | AttributeConverter::toArray($cloudEvent, $this->configuration), 33 | DataFormatter::encode($cloudEvent->getData(), $rawData) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /hack/7.4.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-alpine 2 | 3 | LABEL org.opencontainers.image.url="https://github.com/cloudevents/sdk-php/tree/main/hack/7.4.Dockerfile" \ 4 | org.opencontainers.image.documentation="https://github.com/cloudevents/sdk-php/tree/main/hack/README.md" \ 5 | org.opencontainers.image.source="https://github.com/cloudevents/sdk-php" \ 6 | org.opencontainers.image.vendor="CloudEvent" \ 7 | org.opencontainers.image.title="PHP 7.4" \ 8 | org.opencontainers.image.description="PHP 7.4 test environment for cloudevents/sdk-php" 9 | 10 | COPY --chown=www-data:www-data install-composer /usr/local/bin/install-composer 11 | RUN chmod +x /usr/local/bin/install-composer \ 12 | && /usr/local/bin/install-composer \ 13 | && rm /usr/local/bin/install-composer 14 | 15 | RUN apk update \ 16 | && apk --no-cache upgrade \ 17 | && apk add --no-cache bash ca-certificates git libzip-dev \ 18 | && apk add --no-cache --virtual build-deps autoconf build-base g++ \ 19 | && pecl install pcov \ 20 | && docker-php-ext-enable pcov \ 21 | && apk del --purge build-deps \ 22 | && rm -rf /var/www/html /tmp/pear \ 23 | && chown -R www-data:www-data /var/www 24 | 25 | WORKDIR /var/www 26 | ENTRYPOINT ["/var/www/vendor/bin/phpunit"] 27 | -------------------------------------------------------------------------------- /src/Serializers/DeserializerInterface.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function deserializeBatch(string $payload): array; 29 | 30 | /** 31 | * @param array $attributes 32 | * 33 | * @throws InvalidPayloadSyntaxException 34 | * @throws UnsupportedSpecVersionException 35 | * @throws MissingAttributeException 36 | */ 37 | public function deserializeBinary(string $data, string $contentType, array $attributes): CloudEventInterface; 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | codesniffer: 10 | name: PHP CodeSniffer 11 | runs-on: ubuntu-24.04 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: '7.4' 21 | tools: composer:v2 22 | coverage: none 23 | env: 24 | update: true 25 | 26 | - name: Install Dependencies 27 | uses: nick-invision/retry@v3 28 | with: 29 | timeout_minutes: 5 30 | max_attempts: 5 31 | command: composer update --no-interaction --no-progress 32 | 33 | - name: Execute PHP CodeSniffer 34 | run: vendor/bin/phpcs -n -q --standard=PSR12 ./src ./tests 35 | 36 | psalm: 37 | name: Psalm 38 | runs-on: ubuntu-22.04 39 | 40 | steps: 41 | - name: Checkout code 42 | uses: actions/checkout@v4 43 | 44 | - name: Setup PHP 45 | uses: shivammathur/setup-php@v2 46 | with: 47 | php-version: '7.4' 48 | tools: composer:v2 49 | coverage: none 50 | env: 51 | update: true 52 | 53 | - name: Install Dependencies 54 | uses: nick-invision/retry@v3 55 | with: 56 | timeout_minutes: 5 57 | max_attempts: 5 58 | command: composer update --no-interaction --no-progress 59 | 60 | - name: Execute Psalm 61 | run: vendor/bin/psalm.phar --no-progress --output-format=github 62 | -------------------------------------------------------------------------------- /src/Serializers/Normalizers/V1/Denormalizer.php: -------------------------------------------------------------------------------- 1 | $payload 19 | * 20 | * @throws InvalidAttributeException 21 | * @throws UnsupportedSpecVersionException 22 | * @throws MissingAttributeException 23 | */ 24 | public function denormalize(array $payload): CloudEventInterface 25 | { 26 | try { 27 | if (($payload['specversion'] ?? null) !== CloudEventInterface::SPEC_VERSION) { 28 | throw new UnsupportedSpecVersionException(); 29 | } 30 | 31 | $cloudEvent = AttributeConverter::fromArray($payload); 32 | 33 | if ($cloudEvent === null) { 34 | throw new MissingAttributeException(); 35 | } 36 | 37 | return $cloudEvent->withData(DataFormatter::decode($payload)); 38 | } catch (Throwable $e) { 39 | if ($e instanceof UnsupportedSpecVersionException || $e instanceof MissingAttributeException) { 40 | throw $e; 41 | } 42 | 43 | throw new InvalidAttributeException(null, 0, $e); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/DataFormatterTest.php: -------------------------------------------------------------------------------- 1 | ['foo' => 'bar']], DataFormatter::encode(['foo' => 'bar'], false)); 16 | self::assertEquals(['foo' => 'bar'], DataFormatter::decode(['data' => ['foo' => 'bar']])); 17 | } 18 | 19 | public function testEncodeAndDecodeBinary(): void 20 | { 21 | $data = random_bytes(1024); 22 | $encoded = base64_encode($data); 23 | 24 | self::assertEquals(['data_base64' => $encoded], DataFormatter::encode($data, false)); 25 | self::assertEquals($data, DataFormatter::decode(['data_base64' => $encoded])); 26 | 27 | self::assertEquals(['data' => $data], DataFormatter::encode($data, true)); 28 | self::assertEquals($data, DataFormatter::decode(['data' => $data])); 29 | } 30 | 31 | public function testInvalidBase64Decode1(): void 32 | { 33 | $this->expectException(ValueError::class); 34 | 35 | DataFormatter::decode(['data_base64' => 123]); 36 | } 37 | 38 | public function testInvalidBase64Decode2(): void 39 | { 40 | $this->expectException(ValueError::class); 41 | 42 | DataFormatter::decode(['data_base64' => 'a']); 43 | } 44 | 45 | public function testEncodeAndDecodeEmpty(): void 46 | { 47 | self::assertEquals([], DataFormatter::encode(null, false)); 48 | self::assertEquals(null, DataFormatter::decode([])); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Utilities/DataFormatter.php: -------------------------------------------------------------------------------- 1 | base64_encode($data)]; 24 | } 25 | 26 | return $data !== null ? ['data' => $data] : []; 27 | } 28 | 29 | /** 30 | * @return mixed 31 | */ 32 | public static function decode(array $data) 33 | { 34 | if (isset($data['data_base64'])) { 35 | if (!is_string($data['data_base64'])) { 36 | throw new ValueError( 37 | \sprintf('%s(): Argument #1 ($data) contains bad data_base64 attribute content', __METHOD__) 38 | ); 39 | } 40 | 41 | $decoded = base64_decode($data['data_base64'], true); 42 | 43 | if ($decoded === false) { 44 | throw new ValueError( 45 | \sprintf('%s(): Argument #1 ($data) contains bad data_base64 attribute content', __METHOD__) 46 | ); 47 | } 48 | 49 | return $decoded; 50 | } 51 | 52 | if (isset($data['data'])) { 53 | return $data['data']; 54 | } 55 | 56 | return null; 57 | } 58 | 59 | /** 60 | * @param mixed $data 61 | */ 62 | private static function isBinary($data): bool 63 | { 64 | return is_string($data) && !preg_match('//u', $data); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Http/MarshallerInterface.php: -------------------------------------------------------------------------------- 1 | $cloudEvents 39 | * @param UriInterface|string $uri 40 | * 41 | * @throws UnsupportedSpecVersionException 42 | */ 43 | public function marshalBatchRequest( 44 | array $cloudEvents, 45 | string $method, 46 | $uri 47 | ): RequestInterface; 48 | 49 | /** 50 | * @throws UnsupportedSpecVersionException 51 | */ 52 | public function marshalStructuredResponse( 53 | CloudEventInterface $cloudEvent, 54 | int $code = 200, 55 | string $reasonPhrase = '' 56 | ): ResponseInterface; 57 | 58 | /** 59 | * @throws UnsupportedSpecVersionException 60 | */ 61 | public function marshalBinaryResponse( 62 | CloudEventInterface $cloudEvent, 63 | int $code = 200, 64 | string $reasonPhrase = '' 65 | ): ResponseInterface; 66 | 67 | /** 68 | * @param list $cloudEvents 69 | * 70 | * @throws UnsupportedSpecVersionException 71 | */ 72 | public function marshalBatchResponse( 73 | array $cloudEvents, 74 | int $code = 200, 75 | string $reasonPhrase = '' 76 | ): ResponseInterface; 77 | } 78 | -------------------------------------------------------------------------------- /src/V1/CloudEventImmutable.php: -------------------------------------------------------------------------------- 1 | setId($id); 21 | 22 | return $cloudEvent; 23 | } 24 | 25 | public function withSource(string $source): self 26 | { 27 | $cloudEvent = clone $this; 28 | 29 | $cloudEvent->setSource($source); 30 | 31 | return $cloudEvent; 32 | } 33 | 34 | public function withType(string $type): self 35 | { 36 | $cloudEvent = clone $this; 37 | 38 | $cloudEvent->setType($type); 39 | 40 | return $cloudEvent; 41 | } 42 | 43 | /** 44 | * @param mixed $data 45 | */ 46 | public function withData($data): self 47 | { 48 | $cloudEvent = clone $this; 49 | 50 | $cloudEvent->setData($data); 51 | 52 | return $cloudEvent; 53 | } 54 | 55 | public function withDataContentType(?string $dataContentType): self 56 | { 57 | $cloudEvent = clone $this; 58 | 59 | $cloudEvent->setDataContentType($dataContentType); 60 | 61 | return $cloudEvent; 62 | } 63 | 64 | public function withDataSchema(?string $dataSchema): self 65 | { 66 | $cloudEvent = clone $this; 67 | 68 | $cloudEvent->setDataSchema($dataSchema); 69 | 70 | return $cloudEvent; 71 | } 72 | 73 | public function withSubject(?string $subject): self 74 | { 75 | $cloudEvent = clone $this; 76 | 77 | $cloudEvent->setSubject($subject); 78 | 79 | return $cloudEvent; 80 | } 81 | 82 | public function withTime(?DateTimeImmutable $time): self 83 | { 84 | $cloudEvent = clone $this; 85 | 86 | $cloudEvent->setTime($time); 87 | 88 | return $cloudEvent; 89 | } 90 | 91 | /** 92 | * @param array $extensions 93 | */ 94 | public function withExtensions(array $extensions): self 95 | { 96 | $cloudEvent = clone $this; 97 | 98 | $cloudEvent->setExtensions($extensions); 99 | 100 | return $cloudEvent; 101 | } 102 | 103 | /** 104 | * @param bool|int|string|null $value 105 | */ 106 | public function withExtension(string $attribute, $value): self 107 | { 108 | $cloudEvent = clone $this; 109 | 110 | $cloudEvent->setExtension($attribute, $value); 111 | 112 | return $cloudEvent; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Unit/Serializers/Normalizers/V1/DenormalizerTest.php: -------------------------------------------------------------------------------- 1 | '1.0', 24 | 'id' => '1234-1234-1234', 25 | 'source' => '/var/data', 26 | 'type' => 'com.example.someevent', 27 | 'datacontenttype' => 'application/json', 28 | 'dataschema' => 'com.example/schema', 29 | 'subject' => 'larger-context', 30 | 'time' => '2018-04-05T17:31:00Z', 31 | 'data' => [ 32 | 'key' => 'value', 33 | ], 34 | 'comacme' => 'foo', 35 | ]; 36 | 37 | $formatter = new Denormalizer(); 38 | 39 | $event = $formatter->denormalize($payload); 40 | 41 | self::assertEquals('1.0', $event->getSpecVersion()); 42 | self::assertEquals('1234-1234-1234', $event->getId()); 43 | self::assertEquals('/var/data', $event->getSource()); 44 | self::assertEquals('com.example.someevent', $event->getType()); 45 | self::assertEquals('application/json', $event->getDataContentType()); 46 | self::assertEquals('com.example/schema', $event->getDataSchema()); 47 | self::assertEquals('larger-context', $event->getSubject()); 48 | self::assertEquals(new DateTimeImmutable('2018-04-05T17:31:00Z'), $event->getTime()); 49 | self::assertEquals(['key' => 'value'], $event->getData()); 50 | self::assertEquals(['comacme' => 'foo'], $event->getExtensions()); 51 | } 52 | 53 | public function testDenormalizeMissingId(): void 54 | { 55 | $payload = [ 56 | 'specversion' => '1.0', 57 | 'source' => '/var/data', 58 | 'type' => 'com.example.someevent', 59 | 'datacontenttype' => 'application/json', 60 | 'dataschema' => 'com.example/schema', 61 | 'subject' => 'larger-context', 62 | 'time' => '2018-04-05T17:31:00Z', 63 | 'data' => [ 64 | 'key' => 'value', 65 | ], 66 | 'comacme' => 'foo', 67 | ]; 68 | 69 | $formatter = new Denormalizer(); 70 | 71 | $this->expectException(MissingAttributeException::class); 72 | 73 | $formatter->denormalize($payload); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Utilities/TimeFormatter.php: -------------------------------------------------------------------------------- 1 | $subsecondPrecision 25 | */ 26 | public static function encode(?DateTimeImmutable $time, int $subsecondPrecision): ?string 27 | { 28 | if ($time === null) { 29 | return null; 30 | } 31 | 32 | return sprintf('%sZ', self::encodeWithoutTimezone($time, $subsecondPrecision)); 33 | } 34 | 35 | /** 36 | * @param int<0, 6> $subsecondPrecision 37 | */ 38 | private static function encodeWithoutTimezone(DateTimeImmutable $time, int $subsecondPrecision): string 39 | { 40 | $utcTime = $time->setTimezone(new DateTimeZone(self::TIME_ZONE)); 41 | 42 | if ($subsecondPrecision <= 0) { 43 | return $utcTime->format(self::TIME_FORMAT); 44 | } 45 | 46 | if ($subsecondPrecision >= 6) { 47 | return $utcTime->format(self::TIME_FORMAT_EXTENDED); 48 | } 49 | 50 | return substr($utcTime->format(self::TIME_FORMAT_EXTENDED), 0, $subsecondPrecision - 6); 51 | } 52 | 53 | public static function decode(?string $time): ?DateTimeImmutable 54 | { 55 | if ($time === null) { 56 | return null; 57 | } 58 | 59 | $time = \strtoupper($time); 60 | 61 | /** @psalm-suppress UndefinedFunction */ 62 | $decoded = \str_contains($time, '.') 63 | ? DateTimeImmutable::createFromFormat(self::RFC3339_EXTENDED_FORMAT, self::truncateOverPrecision($time), new DateTimeZone(self::TIME_ZONE)) 64 | : DateTimeImmutable::createFromFormat(self::RFC3339_FORMAT, $time, new DateTimeZone(self::TIME_ZONE)); 65 | 66 | if ($decoded === false) { 67 | throw new ValueError( 68 | \sprintf('%s(): Argument #1 ($time) is not a valid RFC3339 timestamp', __METHOD__) 69 | ); 70 | } 71 | 72 | return $decoded; 73 | } 74 | 75 | private static function truncateOverPrecision(string $time): string 76 | { 77 | [$fst, $snd] = explode('.', $time); 78 | 79 | // match the first n digits at the start 80 | \preg_match('/^\d+/', $snd, $matches); 81 | 82 | $digits = $matches[0] ?? ''; 83 | 84 | // datetime portion + period + up to 6 digits + timezone string 85 | return $fst . '.' . substr($digits, 0, 6) . substr($snd, strlen($digits)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Utilities/AttributeConverter.php: -------------------------------------------------------------------------------- 1 | } $configuration 17 | * 18 | * @return array 19 | */ 20 | public static function toArray(CloudEventInterface $cloudEvent, array $configuration): array 21 | { 22 | /** @var array */ 23 | $attributes = array_filter([ 24 | 'specversion' => $cloudEvent->getSpecVersion(), 25 | 'id' => $cloudEvent->getId(), 26 | 'source' => $cloudEvent->getSource(), 27 | 'type' => $cloudEvent->getType(), 28 | 'datacontenttype' => $cloudEvent->getDataContentType(), 29 | 'dataschema' => $cloudEvent->getDataSchema(), 30 | 'subject' => $cloudEvent->getSubject(), 31 | 'time' => TimeFormatter::encode($cloudEvent->getTime(), $configuration['subsecondPrecision'] ?? 0), 32 | ], fn ($attr) => $attr !== null); 33 | 34 | return array_merge($attributes, $cloudEvent->getExtensions()); 35 | } 36 | 37 | /** 38 | * @param array $attributes 39 | */ 40 | public static function fromArray(array $attributes): ?CloudEventImmutable 41 | { 42 | if (!isset($attributes['id']) || !isset($attributes['source']) || !isset($attributes['type'])) { 43 | return null; 44 | } 45 | 46 | /** @psalm-suppress MixedArgument */ 47 | $cloudEvent = new CloudEventImmutable( 48 | $attributes['id'], 49 | $attributes['source'], 50 | $attributes['type'] 51 | ); 52 | 53 | /** @var mixed $value */ 54 | foreach ($attributes as $attribute => $value) { 55 | switch ($attribute) { 56 | case 'specversion': 57 | case 'id': 58 | case 'source': 59 | case 'type': 60 | case 'data': 61 | case 'data_base64': 62 | break; 63 | case 'datacontenttype': 64 | /** @psalm-suppress MixedArgument */ 65 | $cloudEvent = $cloudEvent->withDataContentType($value); 66 | break; 67 | case 'dataschema': 68 | /** @psalm-suppress MixedArgument */ 69 | $cloudEvent = $cloudEvent->withDataSchema($value); 70 | break; 71 | case 'subject': 72 | /** @psalm-suppress MixedArgument */ 73 | $cloudEvent = $cloudEvent->withSubject($value); 74 | break; 75 | case 'time': 76 | /** @psalm-suppress MixedArgument */ 77 | $cloudEvent = $cloudEvent->withTime(TimeFormatter::decode($value)); 78 | break; 79 | default: 80 | /** @psalm-suppress MixedArgument */ 81 | $cloudEvent = $cloudEvent->withExtension((string) $attribute, $value); 82 | break; 83 | } 84 | } 85 | 86 | return $cloudEvent; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Serializers/JsonSerializer.php: -------------------------------------------------------------------------------- 1 | normalizer = $normalizer; 20 | } 21 | 22 | public static function create(): self 23 | { 24 | return new self( 25 | new Normalizer() 26 | ); 27 | } 28 | 29 | /** 30 | * @throws UnsupportedSpecVersionException 31 | */ 32 | public function serializeStructured(CloudEventInterface $cloudEvent): string 33 | { 34 | if (! $cloudEvent instanceof V1CloudEventInterface) { 35 | throw new UnsupportedSpecVersionException(); 36 | } 37 | 38 | $normalized = $this->normalizer->normalize($cloudEvent, false); 39 | 40 | return json_encode($normalized); 41 | } 42 | 43 | /** 44 | * @param list $cloudEvents 45 | * 46 | * @throws UnsupportedSpecVersionException 47 | */ 48 | public function serializeBatch(array $cloudEvents): string 49 | { 50 | $normalized = []; 51 | 52 | foreach ($cloudEvents as $cloudEvent) { 53 | if (! $cloudEvent instanceof V1CloudEventInterface) { 54 | throw new UnsupportedSpecVersionException(); 55 | } 56 | 57 | $normalized[] = $this->normalizer->normalize($cloudEvent, false); 58 | } 59 | 60 | return json_encode($normalized); 61 | } 62 | 63 | /** 64 | * @throws UnsupportedSpecVersionException 65 | * 66 | * @return array{data: string, contentType: string, attributes: array} 67 | */ 68 | public function serializeBinary(CloudEventInterface $cloudEvent): array 69 | { 70 | if (! $cloudEvent instanceof V1CloudEventInterface) { 71 | throw new UnsupportedSpecVersionException(); 72 | } 73 | 74 | $normalized = $this->normalizer->normalize($cloudEvent, true); 75 | 76 | /** @var string */ 77 | $data = json_encode($normalized['data']); 78 | unset($normalized['data']); 79 | 80 | /** @var string */ 81 | $contentType = $normalized['datacontenttype'] ?? 'application/json'; 82 | unset($normalized['datacontenttype']); 83 | 84 | $attributes = []; 85 | 86 | /** @var mixed $value */ 87 | foreach ($normalized as $key => $value) { 88 | $attributes[$key] = self::encodeAttributeValue($value); 89 | } 90 | 91 | return [ 92 | 'data' => $data, 93 | 'contentType' => $contentType, 94 | 'attributes' => $attributes, 95 | ]; 96 | } 97 | 98 | /** 99 | * @param mixed $value 100 | * 101 | * @return string 102 | */ 103 | private static function encodeAttributeValue($value): string 104 | { 105 | if ($value === true) { 106 | return 'true'; 107 | } 108 | 109 | if ($value === false) { 110 | return 'false'; 111 | } 112 | 113 | return rawurlencode((string) $value); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Serializers/JsonDeserializer.php: -------------------------------------------------------------------------------- 1 | denormalizer = $denormalizer; 22 | } 23 | 24 | public static function create(): self 25 | { 26 | return new self( 27 | new Denormalizer() 28 | ); 29 | } 30 | 31 | /** 32 | * @throws InvalidPayloadSyntaxException 33 | * @throws UnsupportedSpecVersionException 34 | * @throws MissingAttributeException 35 | */ 36 | public function deserializeStructured(string $payload): CloudEventInterface 37 | { 38 | try { 39 | /** @var mixed */ 40 | $decoded = json_decode($payload, true, 512, JSON_THROW_ON_ERROR); 41 | } catch (JsonException $e) { 42 | throw new InvalidPayloadSyntaxException(null, 0, $e); 43 | } 44 | 45 | if (!is_array($decoded)) { 46 | throw new InvalidPayloadSyntaxException(); 47 | } 48 | 49 | return $this->denormalizer->denormalize($decoded); 50 | } 51 | 52 | /** 53 | * @throws InvalidPayloadSyntaxException 54 | * @throws UnsupportedSpecVersionException 55 | * @throws MissingAttributeException 56 | * 57 | * @return list 58 | */ 59 | public function deserializeBatch(string $payload): array 60 | { 61 | try { 62 | $decoded = json_decode($payload, true, 512, JSON_THROW_ON_ERROR); 63 | } catch (JsonException $e) { 64 | throw new InvalidPayloadSyntaxException(null, 0, $e); 65 | } 66 | 67 | if (!is_array($decoded)) { 68 | throw new InvalidPayloadSyntaxException(); 69 | } 70 | 71 | $cloudEvents = []; 72 | 73 | foreach ($decoded as $value) { 74 | if (!is_array($value)) { 75 | throw new InvalidPayloadSyntaxException(); 76 | } 77 | 78 | $cloudEvents[] = $this->denormalizer->denormalize($value); 79 | } 80 | 81 | return $cloudEvents; 82 | } 83 | 84 | /** 85 | * @param array $attributes 86 | * 87 | * @throws InvalidPayloadSyntaxException 88 | * @throws UnsupportedSpecVersionException 89 | * @throws MissingAttributeException 90 | */ 91 | public function deserializeBinary(string $data, string $contentType, array $attributes): CloudEventInterface 92 | { 93 | try { 94 | /** @var mixed */ 95 | $decoded = json_decode($data, true, 512, JSON_THROW_ON_ERROR); 96 | } catch (JsonException $e) { 97 | throw new InvalidPayloadSyntaxException(null, 0, $e); 98 | } 99 | 100 | return $this->denormalizer->denormalize( 101 | array_merge( 102 | array_map('rawurldecode', $attributes), 103 | ['data' => $decoded, 'datacontenttype' => $contentType] 104 | ) 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudevents/sdk-php", 3 | "description": "CloudEvents SDK", 4 | "type": "library", 5 | "license": "Apache-2.0", 6 | "support": { 7 | "issues": "https://github.com/cloudevents/sdk-php/issues", 8 | "source": "https://github.com/cloudevents/sdk-php" 9 | }, 10 | "autoload": { 11 | "psr-4": { 12 | "CloudEvents\\": "src/" 13 | } 14 | }, 15 | "autoload-dev": { 16 | "psr-4": { 17 | "Tests\\": "tests/" 18 | } 19 | }, 20 | "require": { 21 | "php": "^7.4.15 || ^8.0.2", 22 | "ext-json": "*", 23 | "ext-pcre": "*", 24 | "symfony/polyfill-php80": "^1.26" 25 | }, 26 | "require-dev": { 27 | "guzzlehttp/psr7": "^2.4.3", 28 | "php-http/discovery": "^1.15.2", 29 | "phpunit/phpunit": "^9.6.3 || ^10.0.12", 30 | "psalm/phar": "5.26.1", 31 | "psr/http-factory": "^1.0.1", 32 | "psr/http-message": "^1.0.1", 33 | "squizlabs/php_codesniffer": "3.7.2" 34 | }, 35 | "suggest": { 36 | "php-http/discovery": "Required for automatic discovery of HTTP message factories in the HTTP Marshaller.", 37 | "psr/http-factory": "Required for use of the HTTP Marshaller.", 38 | "psr/http-factory-implementation": "Required for use of the HTTP Marshaller.", 39 | "psr/http-message": "Required for use of the HTTP Marshaller and Unmarshaller.", 40 | "psr/http-message-implementation": "Required for use of the HTTP Marshaller and Unmarshaller." 41 | }, 42 | "scripts": { 43 | "lint": "./vendor/bin/phpcs -n --standard=PSR12 ./src ./tests", 44 | "lint-fix": "./vendor/bin/phpcbf -n --standard=PSR12 ./src ./tests", 45 | "tests": "./vendor/bin/phpunit", 46 | "sa": "./vendor/bin/psalm.phar", 47 | "tests-build": [ 48 | "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:7.4-tests -f hack/7.4.Dockerfile hack", 49 | "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.0-tests -f hack/8.0.Dockerfile hack", 50 | "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.1-tests -f hack/8.1.Dockerfile hack", 51 | "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.2-tests -f hack/8.2.Dockerfile hack", 52 | "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.3-tests -f hack/8.3.Dockerfile hack", 53 | "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.4-tests -f hack/8.4.Dockerfile hack" 54 | ], 55 | "tests-docker": [ 56 | "docker run -it -v $(pwd):/var/www cloudevents/sdk-php:7.4-tests --coverage-html=coverage", 57 | "docker run -it -v $(pwd):/var/www cloudevents/sdk-php:8.0-tests", 58 | "docker run -it -v $(pwd):/var/www cloudevents/sdk-php:8.1-tests", 59 | "docker run -it -v $(pwd):/var/www cloudevents/sdk-php:8.2-tests", 60 | "docker run -it -v $(pwd):/var/www cloudevents/sdk-php:8.3-tests", 61 | "docker run -it -v $(pwd):/var/www cloudevents/sdk-php:8.4-tests" 62 | ] 63 | }, 64 | "scripts-descriptions": { 65 | "lint": "Show all current linting errors according to PSR12", 66 | "lint-fix": "Show and fix all current linting errors according to PSR12", 67 | "sa": "Run the static analyzer", 68 | "tests": "Run all tests locally", 69 | "tests-build": "Build containers to test against supported PHP versions", 70 | "tests-docker": "Run tests within supported PHP version containers" 71 | }, 72 | "config": { 73 | "preferred-install": "dist", 74 | "sort-packages": true, 75 | "allow-plugins": { 76 | "php-http/discovery": true 77 | } 78 | }, 79 | "extra": { 80 | "branch-alias": { 81 | "dev-main": "1.2-dev" 82 | } 83 | }, 84 | "minimum-stability": "dev", 85 | "prefer-stable": true 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP SDK for [CloudEvents](https://github.com/cloudevents/spec) 2 | 3 | ## Status 4 | 5 | This SDK currently supports the following versions of CloudEvents: 6 | 7 | - [v1.0](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md) 8 | 9 | ## Installation 10 | 11 | Install the SDK using [Composer](https://getcomposer.org/): 12 | 13 | ```sh 14 | composer require cloudevents/sdk-php 15 | ``` 16 | 17 | ## Create a CloudEvent 18 | 19 | ```php 20 | use CloudEvents\V1\CloudEvent; 21 | use CloudEvents\V1\CloudEventImmutable; 22 | 23 | // Immutable CloudEvent 24 | $immutableEvent = new CloudEventImmutable( 25 | '1n6bFxDMHZFChlI4TVI9tdzphB9', 26 | '/examples/php-sdk', 27 | 'com.example.type', 28 | ['example' => 'first-event'], 29 | 'application/json' 30 | ); 31 | 32 | // Mutable CloudEvent 33 | $mutableEvent = new CloudEvent( 34 | '1n6bFxDMHZFChlI4TVI9tdzphB9', 35 | '/examples/php-sdk', 36 | 'com.example.type', 37 | ['example' => 'first-event'], 38 | 'application/json' 39 | ); 40 | 41 | // Create immutable from mutable or via versa 42 | $event = CloudEventImmutable::createFromInterface($mutableEvent); 43 | $event = CloudEvent::createFromInterface($immutableEvent); 44 | ``` 45 | 46 | ## Serialize/Deserialize a CloudEvent 47 | 48 | ```php 49 | use CloudEvents\Serializers\JsonDeserializer; 50 | use CloudEvents\Serializers\JsonSerializer; 51 | 52 | // JSON serialization 53 | $payload = JsonSerializer::create()->serializeStructured($event); 54 | $payload = JsonSerializer::create()->serializeBatch($events); 55 | 56 | // JSON deserialization 57 | $event = JsonDeserializer::create()->deserializeStructured($payload); 58 | $events = JsonDeserializer::create()->deserializeBatch($payload); 59 | ``` 60 | 61 | ## Marshal/Unmarshal a CloudEvent 62 | 63 | ```php 64 | use CloudEvents\Http\Marshaller; 65 | use CloudEvents\Http\Unmarshaller; 66 | 67 | // Marshal HTTP request 68 | $request = Marshaller::createJsonMarshaller()->marshalStructuredRequest($event); 69 | $request = Marshaller::createJsonMarshaller()->marshalBinaryRequest($event); 70 | $request = Marshaller::createJsonMarshaller()->marshalBatchRequest($events); 71 | 72 | // Marshal HTTP response 73 | $request = Marshaller::createJsonMarshaller()->marshalStructuredResponse($event); 74 | $request = Marshaller::createJsonMarshaller()->marshalBinaryResponse($event); 75 | $request = Marshaller::createJsonMarshaller()->marshalBatchResponse($events); 76 | 77 | // Unmarshal HTTP message 78 | $events = Unmarshaller::createJsonUnmarshaller()->unmarshal($message); 79 | ``` 80 | 81 | ## Testing 82 | 83 | You can use `composer` to build and run test environments when contributing. 84 | 85 | ``` 86 | $ composer run -l 87 | 88 | scripts: 89 | lint Show all current linting errors according to PSR12 90 | lint-fix Show and fix all current linting errors according to PSR12 91 | sa Run the static analyzer 92 | tests Run all tests locally 93 | tests-build Build containers to test against supported PHP versions 94 | tests-docker Run tests within supported PHP version containers 95 | ``` 96 | 97 | ## Community 98 | 99 | - There are bi-weekly calls immediately following the [Serverless/CloudEvents 100 | call](https://github.com/cloudevents/spec#meeting-time) at 101 | 9am PT (US Pacific). Which means they will typically start at 10am PT, but 102 | if the other call ends early then the SDK call will start early as well. 103 | See the [CloudEvents meeting minutes](https://docs.google.com/document/d/1OVF68rpuPK5shIHILK9JOqlZBbfe91RNzQ7u_P7YCDE/edit#) 104 | to determine which week will have the call. 105 | - Slack: #cloudeventssdk channel under 106 | [CNCF's Slack workspace](https://slack.cncf.io/). 107 | - Email: https://lists.cncf.io/g/cncf-cloudevents-sdk 108 | - Contact for additional information: Denis Makogon (`@denysmakogon` on slack). 109 | 110 | Each SDK may have its own unique processes, tooling and guidelines, common 111 | governance related material can be found in the 112 | [CloudEvents `community`](https://github.com/cloudevents/spec#community-and-docs) 113 | directory. In particular, in there you will find information concerning 114 | how SDK projects are 115 | [managed](https://github.com/cloudevents/spec/blob/main/docs/SDK-GOVERNANCE.md), 116 | [guidelines](https://github.com/cloudevents/spec/blob/main/docs/SDK-maintainer-guidelines.md) 117 | for how PR reviews and approval, and our 118 | [Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md) 119 | information. 120 | 121 | If there is a security concern with one of the CloudEvents specifications, or 122 | with one of the project's SDKs, please send an email to 123 | [cncf-cloudevents-security@lists.cncf.io](mailto:cncf-cloudevents-security@lists.cncf.io). 124 | 125 | ## Additional SDK Resources 126 | 127 | - [List of current active maintainers](MAINTAINERS.md) 128 | - [How to contribute to the project](CONTRIBUTING.md) 129 | - [SDK's License](LICENSE) 130 | - [SDK's Release process](RELEASING.md) 131 | -------------------------------------------------------------------------------- /src/Http/Unmarshaller.php: -------------------------------------------------------------------------------- 1 | }> 20 | */ 21 | private array $configuration; 22 | 23 | /** 24 | * @param array}> $configuration 25 | */ 26 | public function __construct(array $configuration) 27 | { 28 | $this->configuration = $configuration; 29 | } 30 | 31 | public static function createJsonUnmarshaller(): self 32 | { 33 | return new self([ 34 | 'json' => ['deserializer' => JsonDeserializer::create(), 'contentTypes' => ['application/json']], 35 | ]); 36 | } 37 | 38 | /** 39 | * @throws InvalidPayloadSyntaxException 40 | * @throws UnsupportedContentTypeException 41 | * @throws UnsupportedSpecVersionException 42 | * @throws MissingAttributeException 43 | * 44 | * @return list 45 | */ 46 | public function unmarshal(MessageInterface $message): array 47 | { 48 | $contentType = strtolower(explode(';', $message->getHeader('Content-Type')[0] ?? '')[0]); 49 | 50 | foreach ($this->configuration as $type => $entry) { 51 | switch ($contentType) { 52 | case sprintf('application/cloudevents+%s', $type): 53 | return self::unmarshalStructured($message, $entry['deserializer']); 54 | case sprintf('application/cloudevents-batch+%s', $type): 55 | return self::unmarshalBatch($message, $entry['deserializer']); 56 | default: 57 | if (in_array($contentType, $entry['contentTypes'], true)) { 58 | return self::unmarshalBinary($message, $entry['deserializer']); 59 | } 60 | } 61 | } 62 | 63 | throw new UnsupportedContentTypeException(); 64 | } 65 | 66 | /** 67 | * @throws InvalidPayloadSyntaxException 68 | * @throws UnsupportedSpecVersionException 69 | * @throws MissingAttributeException 70 | * 71 | * @return list 72 | */ 73 | private static function unmarshalStructured( 74 | MessageInterface $message, 75 | DeserializerInterface $deserializer 76 | ): array { 77 | $cloudEvent = $deserializer->deserializeStructured( 78 | (string) $message->getBody() 79 | ); 80 | 81 | return [$cloudEvent]; 82 | } 83 | 84 | /** 85 | * @throws InvalidPayloadSyntaxException 86 | * @throws UnsupportedSpecVersionException 87 | * @throws MissingAttributeException 88 | * 89 | * @return list 90 | */ 91 | private static function unmarshalBinary( 92 | MessageInterface $message, 93 | DeserializerInterface $deserializer 94 | ): array { 95 | /** @var array> */ 96 | $headers = $message->getHeaders(); 97 | 98 | /** @psalm-suppress MixedArgumentTypeCoercion */ 99 | $cloudEvent = $deserializer->deserializeBinary( 100 | (string) $message->getBody(), 101 | implode(', ', $message->getHeader('Content-Type')), 102 | self::decodeAttributes($headers) 103 | ); 104 | 105 | return [$cloudEvent]; 106 | } 107 | 108 | /** 109 | * @param array> $headers 110 | * 111 | * @return array 112 | */ 113 | private static function decodeAttributes(array $headers): array 114 | { 115 | $attributes = []; 116 | 117 | foreach ($headers as $key => $values) { 118 | $key = (string) $key; 119 | /** @psalm-suppress UndefinedFunction */ 120 | if (\str_starts_with($key, 'ce-')) { 121 | $attributes[substr($key, 3)] = implode(', ', $values); 122 | } 123 | } 124 | 125 | return $attributes; 126 | } 127 | 128 | /** 129 | * @throws InvalidPayloadSyntaxException 130 | * @throws UnsupportedSpecVersionException 131 | * @throws MissingAttributeException 132 | * 133 | * @return list 134 | */ 135 | private static function unmarshalBatch( 136 | MessageInterface $message, 137 | DeserializerInterface $deserializer 138 | ): array { 139 | return $deserializer->deserializeBatch( 140 | (string) $message->getBody() 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/Unit/Serializers/Normalizers/V1/NormalizerTest.php: -------------------------------------------------------------------------------- 1 | createStub(CloudEventInterface::class); 25 | $event->method('getSpecVersion')->willReturn('1.0'); 26 | $event->method('getId')->willReturn('1234-1234-1234'); 27 | $event->method('getSource')->willReturn('/var/data'); 28 | $event->method('getType')->willReturn('com.example.someevent'); 29 | $event->method('getDataContentType')->willReturn('application/json'); 30 | $event->method('getDataSchema')->willReturn('com.example/schema'); 31 | $event->method('getSubject')->willReturn('larger-context'); 32 | $event->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 33 | $event->method('getData')->willReturn(['key' => 'value']); 34 | $event->method('getExtensions')->willReturn(['comacme' => 'foo']); 35 | 36 | $formatter = new Normalizer(); 37 | 38 | self::assertSame( 39 | [ 40 | 'specversion' => '1.0', 41 | 'id' => '1234-1234-1234', 42 | 'source' => '/var/data', 43 | 'type' => 'com.example.someevent', 44 | 'datacontenttype' => 'application/json', 45 | 'dataschema' => 'com.example/schema', 46 | 'subject' => 'larger-context', 47 | 'time' => '2018-04-05T17:31:00Z', 48 | 'comacme' => 'foo', 49 | 'data' => [ 50 | 'key' => 'value', 51 | ], 52 | ], 53 | $formatter->normalize($event, false) 54 | ); 55 | } 56 | 57 | public function testNormalizerWithUnsetAttributes(): void 58 | { 59 | /** @var CloudEventInterface|Stub $event */ 60 | $event = $this->createStub(CloudEventInterface::class); 61 | $event->method('getSpecVersion')->willReturn('1.0'); 62 | $event->method('getId')->willReturn('1234-1234-1234'); 63 | $event->method('getSource')->willReturn('/var/data'); 64 | $event->method('getType')->willReturn('com.example.someevent'); 65 | $event->method('getSubject')->willReturn('larger-context'); 66 | $event->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 67 | $event->method('getExtensions')->willReturn([]); 68 | 69 | $formatter = new Normalizer(); 70 | 71 | self::assertSame( 72 | [ 73 | 'specversion' => '1.0', 74 | 'id' => '1234-1234-1234', 75 | 'source' => '/var/data', 76 | 'type' => 'com.example.someevent', 77 | 'subject' => 'larger-context', 78 | 'time' => '2018-04-05T17:31:00Z', 79 | ], 80 | $formatter->normalize($event, false) 81 | ); 82 | } 83 | 84 | public function testNormalizerWithSubsecondPrecisionConfiguration(): void 85 | { 86 | /** @var CloudEventInterface|Stub $event */ 87 | $event = $this->createStub(CloudEventInterface::class); 88 | $event->method('getSpecVersion')->willReturn('1.0'); 89 | $event->method('getId')->willReturn('1234-1234-1234'); 90 | $event->method('getSource')->willReturn('/var/data'); 91 | $event->method('getType')->willReturn('com.example.someevent'); 92 | $event->method('getDataContentType')->willReturn('application/json'); 93 | $event->method('getDataSchema')->willReturn('com.example/schema'); 94 | $event->method('getSubject')->willReturn('larger-context'); 95 | $event->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00.123456Z')); 96 | $event->method('getData')->willReturn(['key' => 'value']); 97 | $event->method('getExtensions')->willReturn(['comacme' => 'foo']); 98 | 99 | $formatter = new Normalizer(['subsecondPrecision' => 3]); 100 | 101 | self::assertSame( 102 | [ 103 | 'specversion' => '1.0', 104 | 'id' => '1234-1234-1234', 105 | 'source' => '/var/data', 106 | 'type' => 'com.example.someevent', 107 | 'datacontenttype' => 'application/json', 108 | 'dataschema' => 'com.example/schema', 109 | 'subject' => 'larger-context', 110 | 'time' => '2018-04-05T17:31:00.123Z', 111 | 'comacme' => 'foo', 112 | 'data' => [ 113 | 'key' => 'value', 114 | ], 115 | ], 116 | $formatter->normalize($event, false) 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/TimeFormatterTest.php: -------------------------------------------------------------------------------- 1 | expectException(ValueError::class); 133 | 134 | $this->expectExceptionMessage( 135 | 'CloudEvents\\Utilities\\TimeFormatter::decode(): Argument #1 ($time) is not a valid RFC3339 timestamp' 136 | ); 137 | 138 | TimeFormatter::decode($input); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CloudEvents' PHP SDK 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | We welcome contributions from the community! Please take some time to become 6 | acquainted with the process before submitting a pull request. There are just 7 | a few things to keep in mind. 8 | 9 | # Pull Requests 10 | 11 | Typically, a pull request should relate to an existing issue. If you have 12 | found a bug, want to add an improvement, or suggest an API change, please 13 | create an issue before proceeding with a pull request. For very minor changes 14 | such as typos in the documentation this isn't really necessary. 15 | 16 | ## Pull Request Guidelines 17 | 18 | Here you will find step by step guidance for creating, submitting and updating 19 | a pull request in this repository. We hope it will help you have an easy time 20 | managing your work and a positive, satisfying experience when contributing 21 | your code. Thanks for getting involved! :rocket: 22 | 23 | * [Getting Started](#getting-started) 24 | * [Branches](#branches) 25 | * [Commit Messages](#commit-messages) 26 | * [Staying current with main](#staying-current-with-main) 27 | * [Submitting and Updating a Pull Request](#submitting-and-updating-a-pull-request) 28 | * [Congratulations!](#congratulations) 29 | 30 | ## Getting Started 31 | 32 | When creating a pull request, first fork this repository and clone it to your 33 | local development environment. Then add this repository as the upstream. 34 | 35 | ```console 36 | git clone https://github.com/mygithuborg/sdk-php.git 37 | cd sdk-php 38 | git remote add upstream https://github.com/cloudevents/sdk-php.git 39 | ``` 40 | 41 | ## Branches 42 | 43 | The first thing you'll need to do is create a branch for your work. 44 | If you are submitting a pull request that fixes or relates to an existing 45 | GitHub issue, you can use the issue number in your branch name to keep things 46 | organized. 47 | 48 | ```console 49 | git fetch upstream 50 | git reset --hard upstream/main 51 | git checkout FETCH_HEAD 52 | git checkout -b 48-fix-http-agent-error 53 | ``` 54 | 55 | ## Commit Messages 56 | 57 | We prefer that contributors follow the 58 | [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary). 59 | The first line of your commit should be prefixed with a type, be a single 60 | sentence with no period, and succinctly indicate what this commit changes. 61 | 62 | All commit message lines should be kept to fewer than 80 characters if possible. 63 | 64 | An example of a good commit message. 65 | 66 | ```log 67 | docs: remove 0.1, 0.2 spec support from README 68 | ``` 69 | 70 | ### Signing your commits 71 | 72 | Each commit must be signed. Use the `--signoff` flag for your commits. 73 | 74 | ```console 75 | git commit --signoff 76 | ``` 77 | 78 | This will add a line to every git commit message: 79 | 80 | Signed-off-by: Joe Smith 81 | 82 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 83 | 84 | The sign-off is a signature line at the end of your commit message. Your 85 | signature certifies that you wrote the patch or otherwise have the right to pass 86 | it on as open-source code. See [developercertificate.org](http://developercertificate.org/) 87 | for the full text of the certification. 88 | 89 | Be sure to have your `user.name` and `user.email` set in your git config. 90 | If your git config information is set properly then viewing the `git log` 91 | information for your commit will look something like this: 92 | 93 | ``` 94 | Author: Joe Smith 95 | Date: Thu Feb 2 11:41:15 2018 -0800 96 | 97 | Update README 98 | 99 | Signed-off-by: Joe Smith 100 | ``` 101 | 102 | Notice the `Author` and `Signed-off-by` lines match. If they don't your PR will 103 | be rejected by the automated DCO check. 104 | 105 | ## Staying Current with `main` 106 | 107 | As you are working on your branch, changes may happen on `main`. Before 108 | submitting your pull request, be sure that your branch has been updated 109 | with the latest commits. 110 | 111 | ```console 112 | git fetch upstream 113 | git rebase upstream/main 114 | ``` 115 | 116 | This may cause conflicts if the files you are changing on your branch are 117 | also changed on main. Error messages from `git` will indicate if conflicts 118 | exist and what files need attention. Resolve the conflicts in each file, then 119 | continue with the rebase with `git rebase --continue`. 120 | 121 | 122 | If you've already pushed some changes to your `origin` fork, you'll 123 | need to force push these changes. 124 | 125 | ```console 126 | git push -f origin 48-fix-http-agent-error 127 | ``` 128 | 129 | ## Submitting and Updating Your Pull Request 130 | 131 | Before submitting a pull request, you should make sure that all of the tests 132 | successfully pass. 133 | 134 | Once you have sent your pull request, `main` may continue to evolve 135 | before your pull request has landed. If there are any commits on `main` 136 | that conflict with your changes, you may need to update your branch with 137 | these changes before the pull request can land. Resolve conflicts the same 138 | way as before. 139 | 140 | ```console 141 | git fetch upstream 142 | git rebase upstream/main 143 | # fix any potential conflicts 144 | git push -f origin 48-fix-http-agent-error 145 | ``` 146 | 147 | This will cause the pull request to be updated with your changes, and 148 | CI will rerun. 149 | 150 | A maintainer may ask you to make changes to your pull request. Sometimes these 151 | changes are minor and shouldn't appear in the commit log. For example, you may 152 | have a typo in one of your code comments that should be fixed before merge. 153 | You can prevent this from adding noise to the commit log with an interactive 154 | rebase. See the [git documentation](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History) 155 | for details. 156 | 157 | ```console 158 | git commit -m "fixup: fix typo" 159 | git rebase -i upstream/main # follow git instructions 160 | ``` 161 | 162 | Once you have rebased your commits, you can force push to your fork as before. 163 | 164 | ## Congratulations! 165 | 166 | Congratulations! You've done it! We really appreciate the time and energy 167 | you've given to the project. Thank you. 168 | -------------------------------------------------------------------------------- /src/Http/Marshaller.php: -------------------------------------------------------------------------------- 1 | configuration = $configuration; 40 | $this->requestFactory = $requestFactory; 41 | $this->responseFactory = $responseFactory; 42 | $this->streamFactory = $streamFactory; 43 | } 44 | 45 | public static function createJsonMarshaller( 46 | ?RequestFactoryInterface $requestFactory = null, 47 | ?ResponseFactoryInterface $responseFactory = null, 48 | ?StreamFactoryInterface $streamFactory = null 49 | ): self { 50 | return new self( 51 | ['serializer' => JsonSerializer::create(), 'type' => 'json'], 52 | $requestFactory ?? Psr17FactoryDiscovery::findRequestFactory(), 53 | $responseFactory ?? Psr17FactoryDiscovery::findResponseFactory(), 54 | $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory() 55 | ); 56 | } 57 | 58 | /** 59 | * @param UriInterface|string $uri 60 | * 61 | * @throws UnsupportedSpecVersionException 62 | */ 63 | public function marshalStructuredRequest( 64 | CloudEventInterface $cloudEvent, 65 | string $method, 66 | $uri 67 | ): RequestInterface { 68 | $serialized = $this->configuration['serializer']->serializeStructured($cloudEvent); 69 | 70 | return $this->requestFactory->createRequest($method, $uri) 71 | ->withBody($this->streamFactory->createStream($serialized)) 72 | ->withHeader('Content-Type', sprintf('application/cloudevents+%s', $this->configuration['type'])) 73 | ->withHeader('Content-Length', (string) strlen($serialized)); 74 | } 75 | 76 | /** 77 | * @param UriInterface|string $uri 78 | * 79 | * @throws UnsupportedSpecVersionException 80 | */ 81 | public function marshalBinaryRequest( 82 | CloudEventInterface $cloudEvent, 83 | string $method, 84 | $uri 85 | ): RequestInterface { 86 | $serialized = $this->configuration['serializer']->serializeBinary($cloudEvent); 87 | 88 | $request = $this->requestFactory->createRequest($method, $uri) 89 | ->withBody($this->streamFactory->createStream($serialized['data'])) 90 | ->withHeader('Content-Type', $serialized['contentType']) 91 | ->withHeader('Content-Length', (string) strlen($serialized['data'])); 92 | 93 | foreach ($serialized['attributes'] as $key => $value) { 94 | $request = $request->withHeader(sprintf('ce-%s', $key), $value); 95 | } 96 | 97 | return $request; 98 | } 99 | 100 | /** 101 | * @param list $cloudEvents 102 | * @param UriInterface|string $uri 103 | * 104 | * @throws UnsupportedSpecVersionException 105 | */ 106 | public function marshalBatchRequest( 107 | array $cloudEvents, 108 | string $method, 109 | $uri 110 | ): RequestInterface { 111 | $serialized = $this->configuration['serializer']->serializeBatch($cloudEvents); 112 | 113 | return $this->requestFactory->createRequest($method, $uri) 114 | ->withBody($this->streamFactory->createStream($serialized)) 115 | ->withHeader('Content-Type', sprintf('application/cloudevents-batch+%s', $this->configuration['type'])) 116 | ->withHeader('Content-Length', (string) strlen($serialized)); 117 | } 118 | 119 | /** 120 | * @throws UnsupportedSpecVersionException 121 | */ 122 | public function marshalStructuredResponse( 123 | CloudEventInterface $cloudEvent, 124 | int $code = 200, 125 | string $reasonPhrase = '' 126 | ): ResponseInterface { 127 | $serialized = $this->configuration['serializer']->serializeStructured($cloudEvent); 128 | 129 | return $this->responseFactory->createResponse($code, $reasonPhrase) 130 | ->withBody($this->streamFactory->createStream($serialized)) 131 | ->withHeader('Content-Type', sprintf('application/cloudevents+%s', $this->configuration['type'])) 132 | ->withHeader('Content-Length', (string) strlen($serialized)); 133 | } 134 | 135 | /** 136 | * @param UriInterface|string $uri 137 | * 138 | * @throws UnsupportedSpecVersionException 139 | */ 140 | public function marshalBinaryResponse( 141 | CloudEventInterface $cloudEvent, 142 | int $code = 200, 143 | string $reasonPhrase = '' 144 | ): ResponseInterface { 145 | $serialized = $this->configuration['serializer']->serializeBinary($cloudEvent); 146 | 147 | $response = $this->responseFactory->createResponse($code, $reasonPhrase) 148 | ->withBody($this->streamFactory->createStream($serialized['data'])) 149 | ->withHeader('Content-Type', $serialized['contentType']) 150 | ->withHeader('Content-Length', (string) strlen($serialized['data'])); 151 | 152 | foreach ($serialized['attributes'] as $key => $value) { 153 | $response = $response->withHeader(sprintf('ce-%s', $key), $value); 154 | } 155 | 156 | return $response; 157 | } 158 | 159 | /** 160 | * @param list $cloudEvents 161 | * 162 | * @throws UnsupportedSpecVersionException 163 | */ 164 | public function marshalBatchResponse( 165 | array $cloudEvents, 166 | int $code = 200, 167 | string $reasonPhrase = '' 168 | ): ResponseInterface { 169 | $serialized = $this->configuration['serializer']->serializeBatch($cloudEvents); 170 | 171 | return $this->responseFactory->createResponse($code, $reasonPhrase) 172 | ->withBody($this->streamFactory->createStream($serialized)) 173 | ->withHeader('Content-Type', sprintf('application/cloudevents-batch+%s', $this->configuration['type'])) 174 | ->withHeader('Content-Length', (string) strlen($serialized)); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/Unit/V1/CloudEventTest.php: -------------------------------------------------------------------------------- 1 | getEvent()->getSpecVersion()); 27 | } 28 | 29 | public function testGetSetId(): void 30 | { 31 | $event = $this->getEvent(); 32 | self::assertEquals('A234-1234-1234', $event->getId()); 33 | $event = $event->setId('new-id'); 34 | self::assertEquals('new-id', $event->getId()); 35 | } 36 | 37 | public function testGetSetSource(): void 38 | { 39 | $event = $this->getEvent(); 40 | self::assertEquals('https://github.com/cloudevents/spec/pull', $event->getSource()); 41 | $event = $event->setSource('new-source'); 42 | self::assertEquals('new-source', $event->getSource()); 43 | } 44 | 45 | public function testGetSetType(): void 46 | { 47 | $event = $this->getEvent(); 48 | self::assertEquals('com.github.pull_request.opened', $event->getType()); 49 | $event = $event->setType('new-type'); 50 | self::assertEquals('new-type', $event->getType()); 51 | } 52 | 53 | public function testGetSetDataContentType(): void 54 | { 55 | $event = $this->getEvent(); 56 | self::assertEquals('text/xml', $event->getDataContentType()); 57 | $event = $event->setDataContentType('application/json'); 58 | self::assertEquals('application/json', $event->getDataContentType()); 59 | } 60 | 61 | public function testGetSetDataSchema(): void 62 | { 63 | $event = $this->getEvent(); 64 | self::assertEquals(null, $event->getDataSchema()); 65 | $event = $event->setDataSchema('new-schema'); 66 | self::assertEquals('new-schema', $event->getDataSchema()); 67 | } 68 | 69 | public function testGetSetSubject(): void 70 | { 71 | $event = $this->getEvent(); 72 | self::assertEquals('123', $event->getSubject()); 73 | $event = $event->setSubject('new-subject'); 74 | self::assertEquals('new-subject', $event->getSubject()); 75 | } 76 | 77 | public function testGetSetTime(): void 78 | { 79 | $event = $this->getEvent(); 80 | self::assertEquals(new \DateTimeImmutable('2018-04-05T17:31:00Z'), $event->getTime()); 81 | $event = $event->setTime(new \DateTimeImmutable('2021-01-19T17:31:00Z')); 82 | self::assertEquals(new \DateTimeImmutable('2021-01-19T17:31:00Z'), $event->getTime()); 83 | } 84 | 85 | public function testGetSetData(): void 86 | { 87 | $event = $this->getEvent(); 88 | self::assertEquals('', $event->getData()); 89 | $event = $event->setData('{"key": "value"}'); 90 | self::assertEquals('{"key": "value"}', $event->getData()); 91 | } 92 | 93 | public function testCannotSetEmptyExtensionValueType(): void 94 | { 95 | $event = $this->getEvent(); 96 | 97 | $this->expectException(ValueError::class); 98 | 99 | $event->setExtension('', '1.1'); 100 | } 101 | 102 | public function testCannotSetInvalidExtensionAttribute(): void 103 | { 104 | $event = $this->getEvent(); 105 | 106 | $this->expectException(ValueError::class); 107 | 108 | $event->setExtension('comBAD', '1.1'); 109 | } 110 | 111 | public function testCannotSetReservedExtensionAttribute(): void 112 | { 113 | $event = $this->getEvent(); 114 | 115 | $this->expectException(ValueError::class); 116 | 117 | $event->setExtension('specversion', '1.1'); 118 | } 119 | 120 | public function testCannotSetInvalidExtensionValueType(): void 121 | { 122 | $event = $this->getEvent(); 123 | 124 | $this->expectException(TypeError::class); 125 | 126 | $event->setExtension('comacme', 1.1); 127 | } 128 | 129 | public function testCanSetAndUnsetExtensions(): void 130 | { 131 | $event = $this->getEvent(); 132 | self::assertEquals([], $event->getExtensions()); 133 | self::assertNull($event->getExtension('comacme')); 134 | self::assertNull($event->getExtension('comacme2')); 135 | $event = $event->setExtension('comacme', 'foo'); 136 | self::assertEquals('foo', $event->getExtension('comacme')); 137 | self::assertEquals(['comacme' => 'foo'], $event->getExtensions()); 138 | $event = $event->setExtension('comacme2', 123); 139 | self::assertEquals(123, $event->getExtension('comacme2')); 140 | self::assertEquals(['comacme' => 'foo', 'comacme2' => 123], $event->getExtensions()); 141 | $event = $event->setExtension('comacme', null); 142 | self::assertNull($event->getExtension('comacme')); 143 | self::assertEquals(['comacme2' => 123], $event->getExtensions()); 144 | $event = $event->setExtensions(['comacme' => 12345, 'comacme2' => null]); 145 | self::assertEquals(12345, $event->getExtension('comacme')); 146 | self::assertNull($event->getExtension('comacme2')); 147 | self::assertEquals(['comacme' => 12345], $event->getExtensions()); 148 | } 149 | 150 | public function testCanCreateFromInterface(): void 151 | { 152 | $original = $this->getEvent(); 153 | $event = CloudEvent::createFromInterface($original); 154 | self::assertInstanceOf(CloudEvent::class, $original); 155 | self::assertInstanceOf(CloudEventInterface::class, $original); 156 | self::assertInstanceOf(CloudEventInterfaceV1::class, $original); 157 | self::assertSame($original->getId(), $event->getId()); 158 | self::assertSame($original->getSource(), $event->getSource()); 159 | self::assertSame($original->getType(), $event->getType()); 160 | self::assertSame($original->getData(), $event->getData()); 161 | self::assertSame($original->getDataContentType(), $event->getDataContentType()); 162 | self::assertSame($original->getDataSchema(), $event->getDataSchema()); 163 | self::assertSame($original->getSubject(), $event->getSubject()); 164 | self::assertSame($original->getTime(), $event->getTime()); 165 | self::assertSame($original->getExtensions(), $event->getExtensions()); 166 | } 167 | 168 | private function getEvent(): CloudEvent 169 | { 170 | return new CloudEvent( 171 | 'A234-1234-1234', 172 | 'https://github.com/cloudevents/spec/pull', 173 | 'com.github.pull_request.opened', 174 | '', 175 | 'text/xml', 176 | null, 177 | '123', 178 | new \DateTimeImmutable('2018-04-05T17:31:00Z') 179 | ); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /tests/Unit/V1/CloudEventImmutableTest.php: -------------------------------------------------------------------------------- 1 | getEvent()->getSpecVersion()); 27 | } 28 | 29 | public function testGetSetId(): void 30 | { 31 | $event = $this->getEvent(); 32 | self::assertEquals('A234-1234-1234', $event->getId()); 33 | $event = $event->withId('new-id'); 34 | self::assertEquals('new-id', $event->getId()); 35 | } 36 | 37 | public function testGetSetSource(): void 38 | { 39 | $event = $this->getEvent(); 40 | self::assertEquals('https://github.com/cloudevents/spec/pull', $event->getSource()); 41 | $event = $event->withSource('new-source'); 42 | self::assertEquals('new-source', $event->getSource()); 43 | } 44 | 45 | public function testGetSetType(): void 46 | { 47 | $event = $this->getEvent(); 48 | self::assertEquals('com.github.pull_request.opened', $event->getType()); 49 | $event = $event->withType('new-type'); 50 | self::assertEquals('new-type', $event->getType()); 51 | } 52 | 53 | public function testGetSetDataContentType(): void 54 | { 55 | $event = $this->getEvent(); 56 | self::assertEquals('text/xml', $event->getDataContentType()); 57 | $event = $event->withDataContentType('application/json'); 58 | self::assertEquals('application/json', $event->getDataContentType()); 59 | } 60 | 61 | public function testGetSetDataSchema(): void 62 | { 63 | $event = $this->getEvent(); 64 | self::assertEquals(null, $event->getDataSchema()); 65 | $event = $event->withDataSchema('new-schema'); 66 | self::assertEquals('new-schema', $event->getDataSchema()); 67 | } 68 | 69 | public function testGetSetSubject(): void 70 | { 71 | $event = $this->getEvent(); 72 | self::assertEquals('123', $event->getSubject()); 73 | $event = $event->withSubject('new-subject'); 74 | self::assertEquals('new-subject', $event->getSubject()); 75 | } 76 | 77 | public function testGetSetTime(): void 78 | { 79 | $event = $this->getEvent(); 80 | self::assertEquals(new \DateTimeImmutable('2018-04-05T17:31:00Z'), $event->getTime()); 81 | $event = $event->withTime(new \DateTimeImmutable('2021-01-19T17:31:00Z')); 82 | self::assertEquals(new \DateTimeImmutable('2021-01-19T17:31:00Z'), $event->getTime()); 83 | } 84 | 85 | public function testGetSetData(): void 86 | { 87 | $event = $this->getEvent(); 88 | self::assertEquals('', $event->getData()); 89 | $event = $event->withData('{"key": "value"}'); 90 | self::assertEquals('{"key": "value"}', $event->getData()); 91 | } 92 | 93 | public function testCannotSetEmptyExtensionValueType(): void 94 | { 95 | $event = $this->getEvent(); 96 | 97 | $this->expectException(ValueError::class); 98 | 99 | $event->withExtension('', '1.1'); 100 | } 101 | 102 | public function testCannotSetInvalidExtensionAttribute(): void 103 | { 104 | $event = $this->getEvent(); 105 | 106 | $this->expectException(ValueError::class); 107 | 108 | $event->withExtension('comBAD', '1.1'); 109 | } 110 | 111 | public function testCannotSetReservedExtensionAttribute(): void 112 | { 113 | $event = $this->getEvent(); 114 | 115 | $this->expectException(ValueError::class); 116 | 117 | $event->withExtension('specversion', '1.1'); 118 | } 119 | 120 | public function testCannotSetInvalidExtensionValueType(): void 121 | { 122 | $event = $this->getEvent(); 123 | 124 | $this->expectException(TypeError::class); 125 | 126 | $event->withExtension('comacme', 1.1); 127 | } 128 | 129 | public function testCanSetAndUnsetExtensions(): void 130 | { 131 | $event = $this->getEvent(); 132 | self::assertEquals([], $event->getExtensions()); 133 | self::assertNull($event->getExtension('comacme')); 134 | self::assertNull($event->getExtension('comacme2')); 135 | $event = $event->withExtension('comacme', 'foo'); 136 | self::assertEquals('foo', $event->getExtension('comacme')); 137 | self::assertEquals(['comacme' => 'foo'], $event->getExtensions()); 138 | $event = $event->withExtension('comacme2', 123); 139 | self::assertEquals(123, $event->getExtension('comacme2')); 140 | self::assertEquals(['comacme' => 'foo', 'comacme2' => 123], $event->getExtensions()); 141 | $event = $event->withExtension('comacme', null); 142 | self::assertNull($event->getExtension('comacme')); 143 | self::assertEquals(['comacme2' => 123], $event->getExtensions()); 144 | $event = $event->withExtensions(['comacme' => 12345, 'comacme2' => null]); 145 | self::assertEquals(12345, $event->getExtension('comacme')); 146 | self::assertNull($event->getExtension('comacme2')); 147 | self::assertEquals(['comacme' => 12345], $event->getExtensions()); 148 | } 149 | 150 | public function testCanCreateFromInterface(): void 151 | { 152 | $original = $this->getEvent(); 153 | $event = CloudEventImmutable::createFromInterface($original); 154 | self::assertInstanceOf(CloudEventImmutable::class, $original); 155 | self::assertInstanceOf(CloudEventInterface::class, $original); 156 | self::assertInstanceOf(CloudEventInterfaceV1::class, $original); 157 | self::assertSame($original->getId(), $event->getId()); 158 | self::assertSame($original->getSource(), $event->getSource()); 159 | self::assertSame($original->getType(), $event->getType()); 160 | self::assertSame($original->getData(), $event->getData()); 161 | self::assertSame($original->getDataContentType(), $event->getDataContentType()); 162 | self::assertSame($original->getDataSchema(), $event->getDataSchema()); 163 | self::assertSame($original->getSubject(), $event->getSubject()); 164 | self::assertSame($original->getTime(), $event->getTime()); 165 | self::assertSame($original->getExtensions(), $event->getExtensions()); 166 | } 167 | 168 | private function getEvent(): CloudEventImmutable 169 | { 170 | return new CloudEventImmutable( 171 | 'A234-1234-1234', 172 | 'https://github.com/cloudevents/spec/pull', 173 | 'com.github.pull_request.opened', 174 | '', 175 | 'text/xml', 176 | null, 177 | '123', 178 | new \DateTimeImmutable('2018-04-05T17:31:00Z') 179 | ); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/V1/CloudEventTrait.php: -------------------------------------------------------------------------------- 1 | */ 31 | private array $extensions = []; 32 | 33 | /** 34 | * @param mixed $data 35 | * @param array $extensions 36 | */ 37 | public function __construct( 38 | string $id, 39 | string $source, 40 | string $type, 41 | $data = null, 42 | ?string $dataContentType = null, 43 | ?string $dataSchema = null, 44 | ?string $subject = null, 45 | ?DateTimeImmutable $time = null, 46 | array $extensions = [] 47 | ) { 48 | $this->setId($id); 49 | $this->setSource($source); 50 | $this->setType($type); 51 | $this->setData($data); 52 | $this->setDataContentType($dataContentType); 53 | $this->setDataSchema($dataSchema); 54 | $this->setSubject($subject); 55 | $this->setTime($time); 56 | $this->setExtensions($extensions); 57 | } 58 | 59 | public static function createFromInterface(CloudEventInterface $event): self 60 | { 61 | return new self( 62 | $event->getId(), 63 | $event->getSource(), 64 | $event->getType(), 65 | $event->getData(), 66 | $event->getDataContentType(), 67 | $event->getDataSchema(), 68 | $event->getSubject(), 69 | $event->getTime(), 70 | $event->getExtensions() 71 | ); 72 | } 73 | 74 | public function getSpecVersion(): string 75 | { 76 | return CloudEventInterface::SPEC_VERSION; 77 | } 78 | 79 | public function getId(): string 80 | { 81 | return $this->id; 82 | } 83 | 84 | private function setId(string $id): self 85 | { 86 | if ('' === $id) { 87 | throw new ValueError( 88 | \sprintf('%s(): Argument #1 ($id) must be a non-empty string, "" given', __METHOD__) 89 | ); 90 | } 91 | 92 | $this->id = $id; 93 | 94 | return $this; 95 | } 96 | 97 | public function getSource(): string 98 | { 99 | return $this->source; 100 | } 101 | 102 | private function setSource(string $source): self 103 | { 104 | if ('' === $source) { 105 | throw new ValueError( 106 | \sprintf('%s(): Argument #1 ($source) must be a non-empty string, "" given', __METHOD__) 107 | ); 108 | } 109 | 110 | $this->source = $source; 111 | 112 | return $this; 113 | } 114 | 115 | public function getType(): string 116 | { 117 | return $this->type; 118 | } 119 | 120 | private function setType(string $type): self 121 | { 122 | if ('' === $type) { 123 | throw new ValueError( 124 | \sprintf('%s(): Argument #1 ($type) must be a non-empty string, "" given', __METHOD__) 125 | ); 126 | } 127 | 128 | $this->type = $type; 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * @return mixed 135 | */ 136 | public function getData() 137 | { 138 | return $this->data; 139 | } 140 | 141 | /** 142 | * @param mixed $data 143 | */ 144 | private function setData($data): self 145 | { 146 | $this->data = $data; 147 | 148 | return $this; 149 | } 150 | 151 | public function getDataContentType(): ?string 152 | { 153 | return $this->dataContentType; 154 | } 155 | 156 | private function setDataContentType(?string $dataContentType): self 157 | { 158 | if ($dataContentType !== null && \preg_match('#^[-\w]+/[-\w]+$#', $dataContentType) !== 1) { 159 | throw new ValueError( 160 | \sprintf('%s(): Argument #1 ($dataContentType) must be a valid mime-type string or null, "%s" given', __METHOD__, $dataContentType) 161 | ); 162 | } 163 | 164 | $this->dataContentType = $dataContentType; 165 | 166 | return $this; 167 | } 168 | 169 | public function getDataSchema(): ?string 170 | { 171 | return $this->dataSchema; 172 | } 173 | 174 | private function setDataSchema(?string $dataSchema): self 175 | { 176 | if ('' === $dataSchema) { 177 | throw new ValueError( 178 | \sprintf('%s(): Argument #1 ($dataSchema) must be a non-empty string or null, "" given', __METHOD__) 179 | ); 180 | } 181 | 182 | $this->dataSchema = $dataSchema; 183 | 184 | return $this; 185 | } 186 | 187 | public function getSubject(): ?string 188 | { 189 | return $this->subject; 190 | } 191 | 192 | private function setSubject(?string $subject): self 193 | { 194 | if ('' === $subject) { 195 | throw new ValueError( 196 | \sprintf('%s(): Argument #1 ($subject) must be a non-empty string or null, "" given', __METHOD__) 197 | ); 198 | } 199 | 200 | $this->subject = $subject; 201 | 202 | return $this; 203 | } 204 | 205 | public function getTime(): ?DateTimeImmutable 206 | { 207 | return $this->time; 208 | } 209 | 210 | private function setTime(?DateTimeImmutable $time): self 211 | { 212 | $this->time = $time; 213 | 214 | return $this; 215 | } 216 | 217 | /** 218 | * @return array 219 | */ 220 | public function getExtensions(): array 221 | { 222 | return $this->extensions; 223 | } 224 | 225 | /** 226 | * @return bool|int|string|null 227 | */ 228 | public function getExtension(string $attribute) 229 | { 230 | return $this->extensions[$attribute] ?? null; 231 | } 232 | 233 | /** 234 | * @param array $extensions 235 | */ 236 | private function setExtensions(array $extensions): self 237 | { 238 | foreach ($extensions as $attribute => $value) { 239 | $this->setExtension($attribute, $value); 240 | } 241 | 242 | return $this; 243 | } 244 | 245 | /** 246 | * @param bool|int|string|null $value 247 | */ 248 | private function setExtension(string $attribute, $value): self 249 | { 250 | if (\preg_match('/^[a-z0-9]+$/', $attribute) !== 1) { 251 | throw new ValueError( 252 | \sprintf('%s(): Argument #1 ($attribute) must match the regex [a-z0-9]+, %s given', __METHOD__, $attribute) 253 | ); 254 | } 255 | 256 | /** @var list */ 257 | static $reservedAttributes = [ 258 | 'specversion', 259 | 'id', 260 | 'source', 261 | 'type', 262 | 'data', 263 | 'data_base64', 264 | 'datacontenttype', 265 | 'dataschema', 266 | 'subject', 267 | 'time', 268 | ]; 269 | 270 | if (in_array($attribute, $reservedAttributes, true)) { 271 | throw new ValueError( 272 | \sprintf('%s(): Argument #1 ($attribute) must not be a reserved attribute, %s given', __METHOD__, $attribute) 273 | ); 274 | } 275 | 276 | /** @psalm-suppress UndefinedFunction */ 277 | $type = \get_debug_type($value); 278 | $types = ['bool', 'int', 'string', 'null']; 279 | 280 | if (!in_array($type, $types, true)) { 281 | /** @psalm-suppress MixedArgument */ 282 | throw new TypeError( 283 | \sprintf('%s(): Argument #2 ($value) must be of type %s, %s given', __METHOD__, implode('|', $types), $type) 284 | ); 285 | } 286 | 287 | if ($value === null) { 288 | unset($this->extensions[$attribute]); 289 | } else { 290 | $this->extensions[$attribute] = $value; 291 | } 292 | 293 | return $this; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /tests/Unit/Serializers/JsonSerializerTest.php: -------------------------------------------------------------------------------- 1 | createStub(NormalizerInterface::class)) 24 | ); 25 | 26 | self::assertInstanceOf( 27 | SerializerInterface::class, 28 | JsonSerializer::create() 29 | ); 30 | } 31 | 32 | public function testSerializeStructured(): void 33 | { 34 | /** @var CloudEventInterfaceV1|Stub $event */ 35 | $event = $this->createStub(CloudEventInterfaceV1::class); 36 | $event->method('getSpecVersion')->willReturn('1.0'); 37 | $event->method('getId')->willReturn('1234-1234-1234'); 38 | $event->method('getSource')->willReturn('/var/data'); 39 | $event->method('getType')->willReturn('com.example.someevent'); 40 | $event->method('getDataContentType')->willReturn('application/json'); 41 | $event->method('getDataSchema')->willReturn('com.example/schema'); 42 | $event->method('getSubject')->willReturn('larger-context'); 43 | $event->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 44 | $event->method('getData')->willReturn(['key' => 'value']); 45 | $event->method('getExtensions')->willReturn(['comacme' => 'foo']); 46 | 47 | $formatter = JsonSerializer::create(); 48 | 49 | self::assertSame( 50 | '{"specversion":"1.0","id":"1234-1234-1234","source":"\/var\/data","type":"com.example.someevent","datacontenttype":"application\/json","dataschema":"com.example\/schema","subject":"larger-context","time":"2018-04-05T17:31:00Z","comacme":"foo","data":{"key":"value"}}', 51 | $formatter->serializeStructured($event) 52 | ); 53 | } 54 | 55 | public function testSerializeStructuredUnsupportedSpecVersion(): void 56 | { 57 | /** @var CloudEventInterface|Stub $event */ 58 | $event = $this->createStub(CloudEventInterface::class); 59 | $event->method('getSpecVersion')->willReturn('0.3'); 60 | 61 | $formatter = JsonSerializer::create(); 62 | 63 | $this->expectException(UnsupportedSpecVersionException::class); 64 | 65 | $formatter->serializeStructured($event); 66 | } 67 | 68 | public function testSerializeBatch(): void 69 | { 70 | /** @var CloudEventInterfaceV1|Stub $event1 */ 71 | $event1 = $this->createStub(CloudEventInterfaceV1::class); 72 | $event1->method('getSpecVersion')->willReturn('1.0'); 73 | $event1->method('getId')->willReturn('1234-1234-1234'); 74 | $event1->method('getSource')->willReturn('/var/data'); 75 | $event1->method('getType')->willReturn('com.example.someevent'); 76 | $event1->method('getDataContentType')->willReturn('application/json'); 77 | $event1->method('getDataSchema')->willReturn('com.example/schema'); 78 | $event1->method('getSubject')->willReturn('larger-context'); 79 | $event1->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 80 | $event1->method('getData')->willReturn(['key' => 'value']); 81 | $event1->method('getExtensions')->willReturn(['comacme' => 'foo']); 82 | 83 | /** @var CloudEventInterfaceV1|Stub $event2 */ 84 | $event2 = $this->createStub(CloudEventInterfaceV1::class); 85 | $event2->method('getSpecVersion')->willReturn('1.0'); 86 | $event2->method('getId')->willReturn('1234-1234-2222'); 87 | $event2->method('getSource')->willReturn('/var/data'); 88 | $event2->method('getType')->willReturn('com.example.someevent'); 89 | $event2->method('getDataContentType')->willReturn('application/json'); 90 | $event2->method('getDataSchema')->willReturn('com.example/schema'); 91 | $event2->method('getSubject')->willReturn('larger-context'); 92 | $event2->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 93 | $event2->method('getData')->willReturn(['key' => 'value']); 94 | $event2->method('getExtensions')->willReturn(['comacme' => 'foo']); 95 | 96 | $formatter = JsonSerializer::create(); 97 | 98 | self::assertSame( 99 | '[{"specversion":"1.0","id":"1234-1234-1234","source":"\/var\/data","type":"com.example.someevent","datacontenttype":"application\/json","dataschema":"com.example\/schema","subject":"larger-context","time":"2018-04-05T17:31:00Z","comacme":"foo","data":{"key":"value"}},{"specversion":"1.0","id":"1234-1234-2222","source":"\/var\/data","type":"com.example.someevent","datacontenttype":"application\/json","dataschema":"com.example\/schema","subject":"larger-context","time":"2018-04-05T17:31:00Z","comacme":"foo","data":{"key":"value"}}]', 100 | $formatter->serializeBatch([$event1, $event2]) 101 | ); 102 | } 103 | 104 | public function testSerializeBatchUnsupportedSpecVersion(): void 105 | { 106 | /** @var CloudEventInterfaceV1|Stub $event1 */ 107 | $event1 = $this->createStub(CloudEventInterfaceV1::class); 108 | $event1->method('getSpecVersion')->willReturn('1.0'); 109 | $event1->method('getId')->willReturn('1234-1234-1234'); 110 | $event1->method('getSource')->willReturn('/var/data'); 111 | $event1->method('getType')->willReturn('com.example.someevent'); 112 | $event1->method('getDataContentType')->willReturn('application/json'); 113 | $event1->method('getDataSchema')->willReturn('com.example/schema'); 114 | $event1->method('getSubject')->willReturn('larger-context'); 115 | $event1->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 116 | $event1->method('getData')->willReturn(['key' => 'value']); 117 | $event1->method('getExtensions')->willReturn(['comacme' => 'foo']); 118 | 119 | /** @var CloudEventInterface|Stub $event */ 120 | $event2 = $this->createStub(CloudEventInterface::class); 121 | $event2->method('getSpecVersion')->willReturn('0.3'); 122 | 123 | $formatter = JsonSerializer::create(); 124 | 125 | $this->expectException(UnsupportedSpecVersionException::class); 126 | 127 | $formatter->serializeBatch([$event1, $event2]); 128 | } 129 | 130 | public function testSerializeBinary(): void 131 | { 132 | /** @var CloudEventInterfaceV1|Stub $event */ 133 | $event = $this->createStub(CloudEventInterfaceV1::class); 134 | $event->method('getSpecVersion')->willReturn('1.0'); 135 | $event->method('getId')->willReturn('1234-1234-1234'); 136 | $event->method('getSource')->willReturn('/var/data'); 137 | $event->method('getType')->willReturn('com.example.someevent'); 138 | $event->method('getDataContentType')->willReturn('application/json'); 139 | $event->method('getDataSchema')->willReturn('com.example/schema'); 140 | $event->method('getSubject')->willReturn('larger-context'); 141 | $event->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 142 | $event->method('getData')->willReturn(['key' => 'value']); 143 | $event->method('getExtensions')->willReturn(['comacme' => 'foo', 'comacmen' => 123, 'comacmet' => true, 'comacmef' => false]); 144 | 145 | $formatter = JsonSerializer::create(); 146 | 147 | self::assertSame( 148 | [ 149 | 'data' => '{"key":"value"}', 150 | 'contentType' => 'application/json', 151 | 'attributes' => [ 152 | 'specversion' => '1.0', 153 | 'id' => '1234-1234-1234', 154 | 'source' => '%2Fvar%2Fdata', 155 | 'type' => 'com.example.someevent', 156 | 'dataschema' => 'com.example%2Fschema', 157 | 'subject' => 'larger-context', 158 | 'time' => '2018-04-05T17%3A31%3A00Z', 159 | 'comacme' => 'foo', 160 | 'comacmen' => '123', 161 | 'comacmet' => 'true', 162 | 'comacmef' => 'false', 163 | ], 164 | ], 165 | $formatter->serializeBinary($event) 166 | ); 167 | } 168 | 169 | public function testSerializeBinaryUnsupportedSpecVersion(): void 170 | { 171 | /** @var CloudEventInterface|Stub $event */ 172 | $event = $this->createStub(CloudEventInterface::class); 173 | $event->method('getSpecVersion')->willReturn('0.3'); 174 | 175 | $formatter = JsonSerializer::create(); 176 | 177 | $this->expectException(UnsupportedSpecVersionException::class); 178 | 179 | $formatter->serializeBinary($event); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /tests/Unit/Serializers/JsonDeserializerTest.php: -------------------------------------------------------------------------------- 1 | createStub(DenormalizerInterface::class)) 23 | ); 24 | 25 | self::assertInstanceOf( 26 | DeserializerInterface::class, 27 | JsonDeserializer::create() 28 | ); 29 | } 30 | 31 | public function testDeserializeStructured(): void 32 | { 33 | $formatter = JsonDeserializer::create(); 34 | 35 | $event = $formatter->deserializeStructured( 36 | '{"specversion":"1.0","id":"1234-1234-1234","source":"\/var\/data","type":"com.example.someevent","datacontenttype":"application\/json","dataschema":"com.example\/schema","subject":"larger-context","time":"2018-04-05T17:31:00Z","data":{"key":"value"},"comacme":"foo"}' 37 | ); 38 | 39 | self::assertEquals('1.0', $event->getSpecVersion()); 40 | self::assertEquals('1234-1234-1234', $event->getId()); 41 | self::assertEquals('/var/data', $event->getSource()); 42 | self::assertEquals('com.example.someevent', $event->getType()); 43 | self::assertEquals('application/json', $event->getDataContentType()); 44 | self::assertEquals('com.example/schema', $event->getDataSchema()); 45 | self::assertEquals('larger-context', $event->getSubject()); 46 | self::assertEquals(new DateTimeImmutable('2018-04-05T17:31:00Z'), $event->getTime()); 47 | self::assertEquals(['key' => 'value'], $event->getData()); 48 | self::assertEquals(['comacme' => 'foo'], $event->getExtensions()); 49 | } 50 | 51 | public function testDeserializeStructuredUnsupportedSpecVersion(): void 52 | { 53 | $formatter = JsonDeserializer::create(); 54 | 55 | $this->expectException(UnsupportedSpecVersionException::class); 56 | 57 | $formatter->deserializeStructured( 58 | '{"specversion":"0.3","id":"1234-1234-1234","source":"\/var\/data","type":"com.example.someevent","datacontenttype":"application\/json","dataschema":"com.example\/schema","subject":"larger-context","time":"2018-04-05T17:31:00Z","data":{"key":"value"},"comacme":"foo"}' 59 | ); 60 | } 61 | 62 | public function testDeserializeStructuredInvalidSyntax1(): void 63 | { 64 | $formatter = JsonDeserializer::create(); 65 | 66 | $this->expectException(InvalidPayloadSyntaxException::class); 67 | 68 | $formatter->deserializeStructured( 69 | '{"specversion":"1.0","id":"1234-1' 70 | ); 71 | } 72 | 73 | public function testDeserializeStructuredInvalidSyntax2(): void 74 | { 75 | $formatter = JsonDeserializer::create(); 76 | 77 | $this->expectException(InvalidPayloadSyntaxException::class); 78 | 79 | $formatter->deserializeStructured( 80 | 'true' 81 | ); 82 | } 83 | 84 | public function testDeserializeStructuredInvalidId(): void 85 | { 86 | $formatter = JsonDeserializer::create(); 87 | 88 | $this->expectException(InvalidAttributeException::class); 89 | 90 | $formatter->deserializeStructured( 91 | '{"specversion":"1.0","id":123,"source":"\/var\/data","type":"com.example.someevent","datacontenttype":"application\/json","dataschema":"com.example\/schema","subject":"larger-context","time":"2018-04-05T17:31:00Z","data":{"key":"value"},"comacme":"foo"}' 92 | ); 93 | } 94 | 95 | public function testDeserializeBatch(): void 96 | { 97 | $formatter = JsonDeserializer::create(); 98 | 99 | $events = $formatter->deserializeBatch( 100 | '[{"specversion":"1.0","id":"1234-1234-1234","source":"\/var\/data","type":"com.example.someevent","datacontenttype":"application\/json","dataschema":"com.example\/schema","subject":"larger-context","time":"2018-04-05T17:31:00Z","data":{"key":"value"},"comacme":"foo"},{"specversion":"1.0","id":"1234-1234-2222","source":"\/var\/data","type":"com.example.someevent","datacontenttype":"application\/json","dataschema":"com.example\/schema","subject":"larger-context","time":"2018-04-05T17:31:00Z","data":{"key":"value"},"comacme":"foo"}]' 101 | ); 102 | 103 | self::assertCount(2, $events); 104 | 105 | self::assertEquals('1.0', $events[0]->getSpecVersion()); 106 | self::assertEquals('1234-1234-1234', $events[0]->getId()); 107 | self::assertEquals('/var/data', $events[0]->getSource()); 108 | self::assertEquals('com.example.someevent', $events[0]->getType()); 109 | self::assertEquals('application/json', $events[0]->getDataContentType()); 110 | self::assertEquals('com.example/schema', $events[0]->getDataSchema()); 111 | self::assertEquals('larger-context', $events[0]->getSubject()); 112 | self::assertEquals(new DateTimeImmutable('2018-04-05T17:31:00Z'), $events[0]->getTime()); 113 | self::assertEquals(['key' => 'value'], $events[0]->getData()); 114 | self::assertEquals(['comacme' => 'foo'], $events[0]->getExtensions()); 115 | 116 | self::assertEquals('1.0', $events[1]->getSpecVersion()); 117 | self::assertEquals('1234-1234-2222', $events[1]->getId()); 118 | self::assertEquals('/var/data', $events[1]->getSource()); 119 | self::assertEquals('com.example.someevent', $events[1]->getType()); 120 | self::assertEquals('application/json', $events[1]->getDataContentType()); 121 | self::assertEquals('com.example/schema', $events[1]->getDataSchema()); 122 | self::assertEquals('larger-context', $events[1]->getSubject()); 123 | self::assertEquals(new DateTimeImmutable('2018-04-05T17:31:00Z'), $events[1]->getTime()); 124 | self::assertEquals(['key' => 'value'], $events[1]->getData()); 125 | self::assertEquals(['comacme' => 'foo'], $events[1]->getExtensions()); 126 | } 127 | 128 | public function testDeserializeBatchUnsupportedSpecVersion(): void 129 | { 130 | $formatter = JsonDeserializer::create(); 131 | 132 | $this->expectException(UnsupportedSpecVersionException::class); 133 | 134 | $formatter->deserializeBatch( 135 | '[{"specversion":"1.0","id":"1234-1234-1234","source":"\/var\/data","type":"com.example.someevent","datacontenttype":"application\/json","dataschema":"com.example\/schema","subject":"larger-context","time":"2018-04-05T17:31:00Z","data":{"key":"value"},"comacme":"foo"},{"specversion":"0.3","id":"1234-1234-2222","source":"\/var\/data","type":"com.example.someevent","datacontenttype":"application\/json","dataschema":"com.example\/schema","subject":"larger-context","time":"2018-04-05T17:31:00Z","data":{"key":"value"},"comacme":"foo"}]' 136 | ); 137 | } 138 | 139 | public function testDeserializeBatchInvalidSyntax1(): void 140 | { 141 | $formatter = JsonDeserializer::create(); 142 | 143 | $this->expectException(InvalidPayloadSyntaxException::class); 144 | 145 | $formatter->deserializeBatch( 146 | '[{"specversion":"1.0","id":"1234-1' 147 | ); 148 | } 149 | 150 | public function testDeserializeBatchInvalidSyntax2(): void 151 | { 152 | $formatter = JsonDeserializer::create(); 153 | 154 | $this->expectException(InvalidPayloadSyntaxException::class); 155 | 156 | $formatter->deserializeBatch( 157 | 'true' 158 | ); 159 | } 160 | 161 | public function testDeserializeBatchInvalidSyntax3(): void 162 | { 163 | $formatter = JsonDeserializer::create(); 164 | 165 | $this->expectException(InvalidPayloadSyntaxException::class); 166 | 167 | $formatter->deserializeBatch( 168 | '[true]' 169 | ); 170 | } 171 | 172 | public function testDeserializeBinary(): void 173 | { 174 | $formatter = JsonDeserializer::create(); 175 | 176 | $event = $formatter->deserializeBinary( 177 | '{"key":"value"}', 178 | 'application/json', 179 | [ 180 | 'specversion' => '1.0', 181 | 'id' => '1234-1234-1234', 182 | 'source' => '%2Fvar%2Fdata', 183 | 'type' => 'com.example.someevent', 184 | 'dataschema' => 'com.example%2Fschema', 185 | 'subject' => 'larger-context', 186 | 'time' => '2018-04-05T17%3A31%3A00Z', 187 | 'comacme' => 'foo', 188 | ] 189 | ); 190 | 191 | self::assertEquals('1.0', $event->getSpecVersion()); 192 | self::assertEquals('1234-1234-1234', $event->getId()); 193 | self::assertEquals('/var/data', $event->getSource()); 194 | self::assertEquals('com.example.someevent', $event->getType()); 195 | self::assertEquals('application/json', $event->getDataContentType()); 196 | self::assertEquals('com.example/schema', $event->getDataSchema()); 197 | self::assertEquals('larger-context', $event->getSubject()); 198 | self::assertEquals(new DateTimeImmutable('2018-04-05T17:31:00Z'), $event->getTime()); 199 | self::assertEquals(['key' => 'value'], $event->getData()); 200 | self::assertEquals(['comacme' => 'foo'], $event->getExtensions()); 201 | } 202 | 203 | public function testDeserializeBinaryUnsupportedSpecVersion(): void 204 | { 205 | $formatter = JsonDeserializer::create(); 206 | 207 | $this->expectException(UnsupportedSpecVersionException::class); 208 | 209 | $formatter->deserializeBinary( 210 | '{"key":"value"}', 211 | 'application/json', 212 | [ 213 | 'specversion' => '0.3', 214 | 'id' => '1234-1234-1234', 215 | 'source' => '%2Fvar%2Fdata', 216 | 'type' => 'com.example.someevent', 217 | 'dataschema' => 'com.example%2Fschema', 218 | 'subject' => 'larger-context', 219 | 'time' => '2018-04-05T17%3A31%3A00Z', 220 | 'comacme' => 'foo', 221 | ] 222 | ); 223 | } 224 | 225 | public function testDeserializeBinaryInvalidSyntax1(): void 226 | { 227 | $formatter = JsonDeserializer::create(); 228 | 229 | $this->expectException(InvalidPayloadSyntaxException::class); 230 | 231 | $formatter->deserializeBinary( 232 | '{"key":"val', 233 | 'application/json', 234 | [ 235 | 'specversion' => '0.3', 236 | 'id' => '1234-1234-1234', 237 | 'source' => '%2Fvar%2Fdata', 238 | 'type' => 'com.example.someevent', 239 | 'dataschema' => 'com.example%2Fschema', 240 | 'subject' => 'larger-context', 241 | 'time' => '2018-04-05T17%3A31%3A00Z', 242 | 'comacme' => 'foo', 243 | ] 244 | ); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /tests/Unit/Http/MarshallerTest.php: -------------------------------------------------------------------------------- 1 | createStub(CloudEventInterface::class); 28 | $event->method('getSpecVersion')->willReturn('1.0'); 29 | $event->method('getId')->willReturn('1234-1234-1234'); 30 | $event->method('getSource')->willReturn('/var/data'); 31 | $event->method('getType')->willReturn('com.example.someevent'); 32 | $event->method('getDataContentType')->willReturn('application/json'); 33 | $event->method('getDataSchema')->willReturn('com.example/schema'); 34 | $event->method('getSubject')->willReturn('larger-context'); 35 | $event->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 36 | $event->method('getData')->willReturn(['key' => 'value']); 37 | $event->method('getExtensions')->willReturn(['comacme' => 'foo']); 38 | 39 | $request = Marshaller::createJsonMarshaller() 40 | ->marshalStructuredRequest($event, 'GET', 'https://example.com/endpoint'); 41 | 42 | self::assertSame( 43 | "GET /endpoint HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/cloudevents+json\r\nContent-Length: 266\r\n\r\n{\"specversion\":\"1.0\",\"id\":\"1234-1234-1234\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}}", 44 | Message::toString($request) 45 | ); 46 | } 47 | 48 | public function testMarshalBinaryRequest(): void 49 | { 50 | /** @var CloudEventInterfaceV1|Stub $event */ 51 | $event = $this->createStub(CloudEventInterface::class); 52 | $event->method('getSpecVersion')->willReturn('1.0'); 53 | $event->method('getId')->willReturn('1234-1234-1234'); 54 | $event->method('getSource')->willReturn('/var/data'); 55 | $event->method('getType')->willReturn('com.example.someevent'); 56 | $event->method('getDataContentType')->willReturn('application/json'); 57 | $event->method('getDataSchema')->willReturn('com.example/schema'); 58 | $event->method('getSubject')->willReturn('larger-context'); 59 | $event->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 60 | $event->method('getData')->willReturn(['key' => 'value']); 61 | $event->method('getExtensions')->willReturn(['comacme' => 'foo']); 62 | 63 | $request = Marshaller::createJsonMarshaller() 64 | ->marshalBinaryRequest($event, 'GET', 'https://example.com/endpoint'); 65 | 66 | self::assertSame( 67 | "GET /endpoint HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/json\r\nContent-Length: 15\r\nce-specversion: 1.0\r\nce-id: 1234-1234-1234\r\nce-source: %2Fvar%2Fdata\r\nce-type: com.example.someevent\r\nce-dataschema: com.example%2Fschema\r\nce-subject: larger-context\r\nce-time: 2018-04-05T17%3A31%3A00Z\r\nce-comacme: foo\r\n\r\n{\"key\":\"value\"}", 68 | Message::toString($request) 69 | ); 70 | } 71 | 72 | public function testMarshalBatchRequest(): void 73 | { 74 | /** @var CloudEventInterfaceV1|Stub $event */ 75 | $event1 = $this->createStub(CloudEventInterface::class); 76 | $event1->method('getSpecVersion')->willReturn('1.0'); 77 | $event1->method('getId')->willReturn('1234-1234-1234'); 78 | $event1->method('getSource')->willReturn('/var/data'); 79 | $event1->method('getType')->willReturn('com.example.someevent'); 80 | $event1->method('getDataContentType')->willReturn('application/json'); 81 | $event1->method('getDataSchema')->willReturn('com.example/schema'); 82 | $event1->method('getSubject')->willReturn('larger-context'); 83 | $event1->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 84 | $event1->method('getData')->willReturn(['key' => 'value']); 85 | $event1->method('getExtensions')->willReturn(['comacme' => 'foo']); 86 | 87 | /** @var CloudEventInterfaceV1|Stub $event */ 88 | $event2 = $this->createStub(CloudEventInterface::class); 89 | $event2->method('getSpecVersion')->willReturn('1.0'); 90 | $event2->method('getId')->willReturn('1234-1234-2222'); 91 | $event2->method('getSource')->willReturn('/var/data'); 92 | $event2->method('getType')->willReturn('com.example.someevent'); 93 | $event2->method('getDataContentType')->willReturn('application/json'); 94 | $event2->method('getDataSchema')->willReturn('com.example/schema'); 95 | $event2->method('getSubject')->willReturn('larger-context'); 96 | $event2->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 97 | $event2->method('getData')->willReturn(['key' => 'value']); 98 | $event2->method('getExtensions')->willReturn(['comacme' => 'foo']); 99 | 100 | $request = Marshaller::createJsonMarshaller() 101 | ->marshalBatchRequest([$event1, $event2], 'GET', 'https://example.com/endpoint'); 102 | 103 | self::assertSame( 104 | "GET /endpoint HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/cloudevents-batch+json\r\nContent-Length: 535\r\n\r\n[{\"specversion\":\"1.0\",\"id\":\"1234-1234-1234\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}},{\"specversion\":\"1.0\",\"id\":\"1234-1234-2222\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}}]", 105 | Message::toString($request) 106 | ); 107 | } 108 | 109 | public function testMarshalStructuredResponse(): void 110 | { 111 | /** @var CloudEventInterfaceV1|Stub $event */ 112 | $event = $this->createStub(CloudEventInterface::class); 113 | $event->method('getSpecVersion')->willReturn('1.0'); 114 | $event->method('getId')->willReturn('1234-1234-1234'); 115 | $event->method('getSource')->willReturn('/var/data'); 116 | $event->method('getType')->willReturn('com.example.someevent'); 117 | $event->method('getDataContentType')->willReturn('application/json'); 118 | $event->method('getDataSchema')->willReturn('com.example/schema'); 119 | $event->method('getSubject')->willReturn('larger-context'); 120 | $event->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 121 | $event->method('getData')->willReturn(['key' => 'value']); 122 | $event->method('getExtensions')->willReturn(['comacme' => 'foo']); 123 | 124 | $response = Marshaller::createJsonMarshaller() 125 | ->marshalStructuredResponse($event); 126 | 127 | self::assertSame( 128 | "HTTP/1.1 200 OK\r\nContent-Type: application/cloudevents+json\r\nContent-Length: 266\r\n\r\n{\"specversion\":\"1.0\",\"id\":\"1234-1234-1234\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}}", 129 | Message::toString($response) 130 | ); 131 | } 132 | 133 | public function testMarshalBinaryResponse(): void 134 | { 135 | /** @var CloudEventInterfaceV1|Stub $event */ 136 | $event = $this->createStub(CloudEventInterface::class); 137 | $event->method('getSpecVersion')->willReturn('1.0'); 138 | $event->method('getId')->willReturn('1234-1234-1234'); 139 | $event->method('getSource')->willReturn('/var/data'); 140 | $event->method('getType')->willReturn('com.example.someevent'); 141 | $event->method('getDataContentType')->willReturn('application/json'); 142 | $event->method('getDataSchema')->willReturn('com.example/schema'); 143 | $event->method('getSubject')->willReturn('larger-context'); 144 | $event->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 145 | $event->method('getData')->willReturn(['key' => 'value']); 146 | $event->method('getExtensions')->willReturn(['comacme' => 'foo']); 147 | 148 | $response = Marshaller::createJsonMarshaller() 149 | ->marshalBinaryResponse($event); 150 | 151 | self::assertSame( 152 | "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 15\r\nce-specversion: 1.0\r\nce-id: 1234-1234-1234\r\nce-source: %2Fvar%2Fdata\r\nce-type: com.example.someevent\r\nce-dataschema: com.example%2Fschema\r\nce-subject: larger-context\r\nce-time: 2018-04-05T17%3A31%3A00Z\r\nce-comacme: foo\r\n\r\n{\"key\":\"value\"}", 153 | Message::toString($response) 154 | ); 155 | } 156 | 157 | public function testMarshalBatchResponse(): void 158 | { 159 | /** @var CloudEventInterfaceV1|Stub $event */ 160 | $event1 = $this->createStub(CloudEventInterface::class); 161 | $event1->method('getSpecVersion')->willReturn('1.0'); 162 | $event1->method('getId')->willReturn('1234-1234-1234'); 163 | $event1->method('getSource')->willReturn('/var/data'); 164 | $event1->method('getType')->willReturn('com.example.someevent'); 165 | $event1->method('getDataContentType')->willReturn('application/json'); 166 | $event1->method('getDataSchema')->willReturn('com.example/schema'); 167 | $event1->method('getSubject')->willReturn('larger-context'); 168 | $event1->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 169 | $event1->method('getData')->willReturn(['key' => 'value']); 170 | $event1->method('getExtensions')->willReturn(['comacme' => 'foo']); 171 | 172 | /** @var CloudEventInterfaceV1|Stub $event */ 173 | $event2 = $this->createStub(CloudEventInterface::class); 174 | $event2->method('getSpecVersion')->willReturn('1.0'); 175 | $event2->method('getId')->willReturn('1234-1234-2222'); 176 | $event2->method('getSource')->willReturn('/var/data'); 177 | $event2->method('getType')->willReturn('com.example.someevent'); 178 | $event2->method('getDataContentType')->willReturn('application/json'); 179 | $event2->method('getDataSchema')->willReturn('com.example/schema'); 180 | $event2->method('getSubject')->willReturn('larger-context'); 181 | $event2->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00Z')); 182 | $event2->method('getData')->willReturn(['key' => 'value']); 183 | $event2->method('getExtensions')->willReturn(['comacme' => 'foo']); 184 | 185 | $response = Marshaller::createJsonMarshaller() 186 | ->marshalBatchResponse([$event1, $event2]); 187 | 188 | self::assertSame( 189 | "HTTP/1.1 200 OK\r\nContent-Type: application/cloudevents-batch+json\r\nContent-Length: 535\r\n\r\n[{\"specversion\":\"1.0\",\"id\":\"1234-1234-1234\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}},{\"specversion\":\"1.0\",\"id\":\"1234-1234-2222\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}}]", 190 | Message::toString($response) 191 | ); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /tests/Unit/Http/UnmarshallerTest.php: -------------------------------------------------------------------------------- 1 | unmarshal( 27 | Message::parseRequest( 28 | "GET /endpoint HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/cloudevents+json\r\nContent-Length: 266\r\n\r\n{\"specversion\":\"1.0\",\"id\":\"1234-1234-1234\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}}" 29 | ) 30 | ); 31 | 32 | self::assertCount(1, $events); 33 | 34 | self::assertInstanceOf(CloudEventInterface::class, $events[0]); 35 | self::assertSame('1.0', $events[0]->getSpecVersion()); 36 | self::assertSame('1234-1234-1234', $events[0]->getId()); 37 | self::assertSame('/var/data', $events[0]->getSource()); 38 | self::assertSame('com.example.someevent', $events[0]->getType()); 39 | self::assertSame('application/json', $events[0]->getDataContentType()); 40 | self::assertSame('com.example/schema', $events[0]->getDataSchema()); 41 | self::assertSame('larger-context', $events[0]->getSubject()); 42 | self::assertSame(1522949460, $events[0]->getTime()->getTimestamp()); 43 | self::assertSame(['key' => 'value'], $events[0]->getData()); 44 | self::assertSame(['comacme' => 'foo'], $events[0]->getExtensions()); 45 | } 46 | 47 | public function testUnmarshalStructuredRequestInvalidContentType(): void 48 | { 49 | $this->expectException(UnsupportedContentTypeException::class); 50 | 51 | Unmarshaller::createJsonUnmarshaller()->unmarshal( 52 | Message::parseRequest( 53 | "GET /endpoint HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/cloudevents+xml\r\nContent-Length: 4\r\n\r\nDATA" 54 | ) 55 | ); 56 | } 57 | 58 | public function testUnmarshalBinaryRequest(): void 59 | { 60 | $events = Unmarshaller::createJsonUnmarshaller()->unmarshal( 61 | Message::parseRequest( 62 | "GET /endpoint HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/json\r\nContent-Length: 15\r\nce-specversion: 1.0\r\nce-id: 1234-1234-1234\r\nce-source: %2Fvar%2Fdata\r\nce-type: com.example.someevent\r\nce-dataschema: com.example%2Fschema\r\nce-subject: larger-context\r\nce-time: 2018-04-05T17%3A31%3A00Z\r\nce-comacme: foo\r\n\r\n{\"key\":\"value\"}" 63 | ) 64 | ); 65 | 66 | self::assertCount(1, $events); 67 | 68 | self::assertInstanceOf(CloudEventInterface::class, $events[0]); 69 | self::assertSame('1.0', $events[0]->getSpecVersion()); 70 | self::assertSame('1234-1234-1234', $events[0]->getId()); 71 | self::assertSame('/var/data', $events[0]->getSource()); 72 | self::assertSame('com.example.someevent', $events[0]->getType()); 73 | self::assertSame('application/json', $events[0]->getDataContentType()); 74 | self::assertSame('com.example/schema', $events[0]->getDataSchema()); 75 | self::assertSame('larger-context', $events[0]->getSubject()); 76 | self::assertSame(1522949460, $events[0]->getTime()->getTimestamp()); 77 | self::assertSame(['key' => 'value'], $events[0]->getData()); 78 | self::assertSame(['comacme' => 'foo'], $events[0]->getExtensions()); 79 | } 80 | 81 | public function testUnmarshalBinaryRequestInvalidContentType(): void 82 | { 83 | $this->expectException(UnsupportedContentTypeException::class); 84 | 85 | Unmarshaller::createJsonUnmarshaller()->unmarshal( 86 | Message::parseRequest( 87 | "GET /endpoint HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/xml\r\nContent-Length: 4\r\nce-specversion: 1.0\r\nce-id: 1234-1234-1234\r\nce-source: %2Fvar%2Fdata\r\nce-type: com.example.someevent\r\nce-dataschema: com.example%2Fschema\r\nce-subject: larger-context\r\nce-time: 2018-04-05T17%3A31%3A00Z\r\nce-comacme: foo\r\n\r\nDATA" 88 | ) 89 | ); 90 | } 91 | 92 | public function testUnmarshalBatchRequest(): void 93 | { 94 | $events = Unmarshaller::createJsonUnmarshaller()->unmarshal( 95 | Message::parseRequest( 96 | "GET /endpoint HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/cloudevents-batch+json\r\nContent-Length: 535\r\n\r\n[{\"specversion\":\"1.0\",\"id\":\"1234-1234-1234\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}},{\"specversion\":\"1.0\",\"id\":\"1234-1234-2222\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}}]" 97 | ) 98 | ); 99 | 100 | self::assertCount(2, $events); 101 | 102 | self::assertInstanceOf(CloudEventInterface::class, $events[0]); 103 | self::assertSame('1.0', $events[0]->getSpecVersion()); 104 | self::assertSame('1234-1234-1234', $events[0]->getId()); 105 | self::assertSame('/var/data', $events[0]->getSource()); 106 | self::assertSame('com.example.someevent', $events[0]->getType()); 107 | self::assertSame('application/json', $events[0]->getDataContentType()); 108 | self::assertSame('com.example/schema', $events[0]->getDataSchema()); 109 | self::assertSame('larger-context', $events[0]->getSubject()); 110 | self::assertSame(1522949460, $events[0]->getTime()->getTimestamp()); 111 | self::assertSame(['key' => 'value'], $events[0]->getData()); 112 | self::assertSame(['comacme' => 'foo'], $events[0]->getExtensions()); 113 | 114 | self::assertInstanceOf(CloudEventInterface::class, $events[1]); 115 | self::assertSame('1.0', $events[1]->getSpecVersion()); 116 | self::assertSame('1234-1234-2222', $events[1]->getId()); 117 | self::assertSame('/var/data', $events[1]->getSource()); 118 | self::assertSame('com.example.someevent', $events[1]->getType()); 119 | self::assertSame('application/json', $events[1]->getDataContentType()); 120 | self::assertSame('com.example/schema', $events[1]->getDataSchema()); 121 | self::assertSame('larger-context', $events[1]->getSubject()); 122 | self::assertSame(1522949460, $events[1]->getTime()->getTimestamp()); 123 | self::assertSame(['key' => 'value'], $events[1]->getData()); 124 | self::assertSame(['comacme' => 'foo'], $events[1]->getExtensions()); 125 | } 126 | 127 | public function testUnmarshalStructuredResponse(): void 128 | { 129 | $events = Unmarshaller::createJsonUnmarshaller()->unmarshal( 130 | Message::parseResponse( 131 | "HTTP/1.1 200 OK\r\nContent-Type: application/cloudevents+json\r\nContent-Length: 266\r\n\r\n{\"specversion\":\"1.0\",\"id\":\"1234-1234-1234\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}}" 132 | ) 133 | ); 134 | 135 | self::assertCount(1, $events); 136 | 137 | self::assertInstanceOf(CloudEventInterface::class, $events[0]); 138 | self::assertSame('1.0', $events[0]->getSpecVersion()); 139 | self::assertSame('1234-1234-1234', $events[0]->getId()); 140 | self::assertSame('/var/data', $events[0]->getSource()); 141 | self::assertSame('com.example.someevent', $events[0]->getType()); 142 | self::assertSame('application/json', $events[0]->getDataContentType()); 143 | self::assertSame('com.example/schema', $events[0]->getDataSchema()); 144 | self::assertSame('larger-context', $events[0]->getSubject()); 145 | self::assertSame(1522949460, $events[0]->getTime()->getTimestamp()); 146 | self::assertSame(['key' => 'value'], $events[0]->getData()); 147 | self::assertSame(['comacme' => 'foo'], $events[0]->getExtensions()); 148 | } 149 | 150 | public function testUnmarshalStructuredResponseInvalidContentType(): void 151 | { 152 | $this->expectException(UnsupportedContentTypeException::class); 153 | 154 | Unmarshaller::createJsonUnmarshaller()->unmarshal( 155 | Message::parseResponse( 156 | "HTTP/1.1 200 OK\r\nContent-Type: application/cloudevents+xml\r\nContent-Length: 4\r\n\r\nDATA" 157 | ) 158 | ); 159 | } 160 | 161 | public function testUnmarshalBinaryResponse(): void 162 | { 163 | $events = Unmarshaller::createJsonUnmarshaller()->unmarshal( 164 | Message::parseResponse( 165 | "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 15\r\nce-specversion: 1.0\r\nce-id: 1234-1234-1234\r\nce-source: %2Fvar%2Fdata\r\nce-type: com.example.someevent\r\nce-dataschema: com.example%2Fschema\r\nce-subject: larger-context\r\nce-time: 2018-04-05T17%3A31%3A00Z\r\nce-comacme: foo\r\n\r\n{\"key\":\"value\"}" 166 | ) 167 | ); 168 | 169 | self::assertCount(1, $events); 170 | 171 | self::assertInstanceOf(CloudEventInterface::class, $events[0]); 172 | self::assertSame('1.0', $events[0]->getSpecVersion()); 173 | self::assertSame('1234-1234-1234', $events[0]->getId()); 174 | self::assertSame('/var/data', $events[0]->getSource()); 175 | self::assertSame('com.example.someevent', $events[0]->getType()); 176 | self::assertSame('application/json', $events[0]->getDataContentType()); 177 | self::assertSame('com.example/schema', $events[0]->getDataSchema()); 178 | self::assertSame('larger-context', $events[0]->getSubject()); 179 | self::assertSame(1522949460, $events[0]->getTime()->getTimestamp()); 180 | self::assertSame(['key' => 'value'], $events[0]->getData()); 181 | self::assertSame(['comacme' => 'foo'], $events[0]->getExtensions()); 182 | } 183 | 184 | public function testUnmarshalBatchResponse(): void 185 | { 186 | $events = Unmarshaller::createJsonUnmarshaller()->unmarshal( 187 | Message::parseResponse( 188 | "HTTP/1.1 200 OK\r\nContent-Type: application/cloudevents-batch+json\r\nContent-Length: 535\r\n\r\n[{\"specversion\":\"1.0\",\"id\":\"1234-1234-1234\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}},{\"specversion\":\"1.0\",\"id\":\"1234-1234-2222\",\"source\":\"\/var\/data\",\"type\":\"com.example.someevent\",\"datacontenttype\":\"application\/json\",\"dataschema\":\"com.example\/schema\",\"subject\":\"larger-context\",\"time\":\"2018-04-05T17:31:00Z\",\"comacme\":\"foo\",\"data\":{\"key\":\"value\"}}]" 189 | ) 190 | ); 191 | 192 | self::assertCount(2, $events); 193 | 194 | self::assertInstanceOf(CloudEventInterface::class, $events[0]); 195 | self::assertSame('1.0', $events[0]->getSpecVersion()); 196 | self::assertSame('1234-1234-1234', $events[0]->getId()); 197 | self::assertSame('/var/data', $events[0]->getSource()); 198 | self::assertSame('com.example.someevent', $events[0]->getType()); 199 | self::assertSame('application/json', $events[0]->getDataContentType()); 200 | self::assertSame('com.example/schema', $events[0]->getDataSchema()); 201 | self::assertSame('larger-context', $events[0]->getSubject()); 202 | self::assertSame(1522949460, $events[0]->getTime()->getTimestamp()); 203 | self::assertSame(['key' => 'value'], $events[0]->getData()); 204 | self::assertSame(['comacme' => 'foo'], $events[0]->getExtensions()); 205 | 206 | self::assertInstanceOf(CloudEventInterface::class, $events[1]); 207 | self::assertSame('1.0', $events[1]->getSpecVersion()); 208 | self::assertSame('1234-1234-2222', $events[1]->getId()); 209 | self::assertSame('/var/data', $events[1]->getSource()); 210 | self::assertSame('com.example.someevent', $events[1]->getType()); 211 | self::assertSame('application/json', $events[1]->getDataContentType()); 212 | self::assertSame('com.example/schema', $events[1]->getDataSchema()); 213 | self::assertSame('larger-context', $events[1]->getSubject()); 214 | self::assertSame(1522949460, $events[1]->getTime()->getTimestamp()); 215 | self::assertSame(['key' => 'value'], $events[1]->getData()); 216 | self::assertSame(['comacme' => 'foo'], $events[1]->getExtensions()); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | The PSR-12 coding standard. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 0 47 | 48 | 49 | 0 50 | 51 | 52 | 0 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | error 159 | Method name "%s" must not be prefixed with an underscore to indicate visibility 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 0 206 | 207 | 208 | 0 209 | 210 | 211 | 212 | 213 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 0 234 | 235 | 236 | 0 237 | 238 | 239 | 240 | 241 | 242 | 243 | 0 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 0 295 | 296 | 297 | 0 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | --------------------------------------------------------------------------------