├── 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 |
--------------------------------------------------------------------------------