├── .github
├── SECURITY.md
├── CODEOWNERS
├── workflows
│ ├── ci.yml
│ └── stale.yml
└── CONTRIBUTING.md
├── .gitignore
├── CHANGELOG.md
├── src
├── Section
│ ├── SectionInterface.php
│ └── Section.php
├── Exception
│ ├── ExceptionInterface.php
│ ├── InvalidArgumentException.php
│ ├── InvalidCredentials.php
│ └── InvalidServerResponse.php
├── CardInterface.php
├── Actions
│ ├── ActionInterface.php
│ ├── OpenUriAction.php
│ ├── HttpPostAction.php
│ └── ActionCard.php
├── Inputs
│ ├── InputInterface.php
│ ├── DateInput.php
│ ├── TextInput.php
│ └── MultiChoiceInput.php
├── RequestBuilder.php
├── Client.php
└── Card.php
├── tests
├── ClientUnitTest.php
├── Actions
│ ├── OpenUriActionUnitTest.php
│ ├── HttpPostActionUnitTest.php
│ └── ActionCardUnitTest.php
├── Inputs
│ ├── DateInputUnitTest.php
│ ├── TextInputUnitTest.php
│ └── MultiChoiceInputUnitTest.php
├── CardUnitTest.php
├── SectionUnitTest.php
└── ClientFunctionalTest.php
├── .php-cs-fixer.php
├── phpunit.xml.dist
├── LICENSE
├── composer.json
└── README.md
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | ## Security Policy
2 |
3 | If you discover any security related issues, please email opensource@skrepr.com instead of using the issue tracker...
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 |
3 | # PHPStorm
4 | .idea
5 | .idea/
6 | .idea/*
7 |
8 | # Composer
9 | composer.lock
10 |
11 | # PHPUnit
12 | .phpunit.result.cache
13 | build
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Review when someone opens a pull request.
2 | * @EJTJ3
3 |
4 | # Change in .github can affect the way the repo is managed. Because of this, team infra needs to review the changes
5 | .github/ @skrepr/infra
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | CHANGELOG for 0.1.0
2 | ===================
3 |
4 | This changelog references the relevant changes (bug and security fixes) done
5 | in 0.0 minor versions.
6 |
7 | # 0.1.0 (2021-09-23)
8 | * initial release
9 |
10 |
--------------------------------------------------------------------------------
/src/Section/SectionInterface.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | interface SectionInterface
11 | {
12 | public function toArray(): array;
13 | }
14 |
--------------------------------------------------------------------------------
/src/Exception/ExceptionInterface.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | interface ExceptionInterface extends Throwable
13 | {
14 | }
15 |
--------------------------------------------------------------------------------
/src/Exception/InvalidArgumentException.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
11 | {
12 | }
13 |
--------------------------------------------------------------------------------
/src/Exception/InvalidCredentials.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | final class InvalidCredentials extends RuntimeException implements ExceptionInterface
13 | {
14 | }
15 |
--------------------------------------------------------------------------------
/src/Exception/InvalidServerResponse.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | final class InvalidServerResponse extends RuntimeException implements ExceptionInterface
13 | {
14 | }
15 |
--------------------------------------------------------------------------------
/src/CardInterface.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | interface CardInterface
11 | {
12 | public const STATUS_SUCCESS = '#01BC36';
13 |
14 | public const STATUS_DEFAULT = '#0076D7';
15 |
16 | public const STATUS_FAILURE = '#FF0000';
17 |
18 | public function toArray(): array;
19 | }
20 |
--------------------------------------------------------------------------------
/src/Actions/ActionInterface.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | interface ActionInterface
11 | {
12 | public const ACTION_CARD = 'ActionCard';
13 |
14 | public const HTTP_POST_ACTION = 'HttpPOST';
15 |
16 | public const OPEN_URI_ACTION = 'OpenUri';
17 |
18 | public function getName(): string;
19 |
20 | public function toArray(): array;
21 | }
22 |
--------------------------------------------------------------------------------
/src/Inputs/InputInterface.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | interface InputInterface
11 | {
12 | public const DATE_INPUT = 'DateInput';
13 |
14 | public const MULTI_CHOICE_INPUT = 'MultichoiceInput';
15 |
16 | public const TEXT_INPUT = 'TextInput';
17 |
18 | public function getId(): string;
19 |
20 | public function getTitle(): string;
21 |
22 | public function toArray(): array;
23 | }
24 |
--------------------------------------------------------------------------------
/tests/ClientUnitTest.php:
--------------------------------------------------------------------------------
1 | createClient();
15 |
16 | $this->assertSame('http://fake.endpoint', $client->getEndPoint());
17 | }
18 |
19 | private function createClient(): Client
20 | {
21 | return new Client('http://fake.endpoint');
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | in(__DIR__)
7 | ;
8 |
9 | return (new PhpCsFixer\Config())
10 | ->setRules([
11 | '@Symfony' => true,
12 | 'phpdoc_trim_consecutive_blank_line_separation' => true,
13 | 'yoda_style' => false,
14 | 'concat_space' => ['spacing' => 'one'],
15 | 'array_syntax' => ['syntax' => 'short'],
16 | 'phpdoc_order' => true,
17 | 'no_superfluous_phpdoc_tags' => true,
18 | 'phpdoc_align' => false,
19 | 'class_attributes_separation' => true,
20 | ])
21 | ->setFinder($finder);
22 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on: [ pull_request ]
3 |
4 | jobs:
5 | phpunit:
6 | name: PHPUnit
7 | runs-on: ubuntu-latest
8 | strategy:
9 | max-parallel: 10
10 | matrix:
11 | php: [ '7.4', '8.0' ]
12 |
13 | steps:
14 | - name: Set up PHP
15 | uses: shivammathur/setup-php@v2
16 | with:
17 | php-version: ${{ matrix.php }}
18 | coverage: none
19 |
20 | - name: Checkout code
21 | uses: actions/checkout@v2
22 |
23 | - name: Download dependencies
24 | uses: ramsey/composer-install@v1
25 |
26 | - name: Run tests
27 | run: ./vendor/bin/phpunit
28 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | tests
16 |
17 |
18 |
19 |
20 | ./src
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Skrepr
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Inputs/DateInput.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class DateInput implements InputInterface
11 | {
12 | private string $id;
13 |
14 | private string $title;
15 |
16 | public function __construct(string $id, string $title)
17 | {
18 | $this->id = $id;
19 | $this->title = $title;
20 | }
21 |
22 | public function getId(): string
23 | {
24 | return $this->id;
25 | }
26 |
27 | public function setId(string $id): self
28 | {
29 | $this->id = $id;
30 |
31 | return $this;
32 | }
33 |
34 | public function getTitle(): string
35 | {
36 | return $this->title;
37 | }
38 |
39 | public function setTitle(string $title): self
40 | {
41 | $this->title = $title;
42 |
43 | return $this;
44 | }
45 |
46 | public function toArray(): array
47 | {
48 | return [
49 | '@type' => InputInterface::DATE_INPUT,
50 | 'id' => $this->getId(),
51 | 'title' => $this->getTitle(),
52 | ];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: 'Close stale issues and PR'
2 | on:
3 | schedule:
4 | - cron: '0 0 1 * *'
5 |
6 | jobs:
7 | stale:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/stale@v4
11 | with:
12 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
13 | stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
14 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
15 | stale-issue-label: 'no-issue-activity'
16 | exempt-issue-labels: 'awaiting-approval,work-in-progress'
17 | stale-pr-label: 'no-pr-activity'
18 | exempt-pr-labels: 'awaiting-approval,work-in-progress'
19 | only-labels: 'awaiting-feedback,awaiting-answers'
20 | days-before-stale: 30
21 | days-before-close: 5
22 | days-before-pr-close: -1
23 |
--------------------------------------------------------------------------------
/src/Actions/OpenUriAction.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class OpenUriAction implements ActionInterface
11 | {
12 | private string $name;
13 |
14 | private string $target;
15 |
16 | public function __construct(string $name, string $target)
17 | {
18 | $this->name = $name;
19 | $this->target = $target;
20 | }
21 |
22 | public function getName(): string
23 | {
24 | return $this->name;
25 | }
26 |
27 | public function setName(string $name): self
28 | {
29 | $this->name = $name;
30 |
31 | return $this;
32 | }
33 |
34 | public function getTarget(): string
35 | {
36 | return $this->target;
37 | }
38 |
39 | public function setTarget(string $target): self
40 | {
41 | $this->target = $target;
42 |
43 | return $this;
44 | }
45 |
46 | public function toArray(): array
47 | {
48 | return [
49 | '@type' => ActionInterface::OPEN_URI_ACTION,
50 | 'name' => $this->name,
51 | 'target' => $this->target,
52 | ];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Actions/HttpPostAction.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class HttpPostAction implements ActionInterface
11 | {
12 | private string $name;
13 |
14 | private string $target;
15 |
16 | public function __construct(string $name, string $target)
17 | {
18 | $this->name = $name;
19 | $this->target = $target;
20 | }
21 |
22 | public function getName(): string
23 | {
24 | return $this->name;
25 | }
26 |
27 | public function setName(string $name): self
28 | {
29 | $this->name = $name;
30 |
31 | return $this;
32 | }
33 |
34 | public function getTarget(): string
35 | {
36 | return $this->target;
37 | }
38 |
39 | public function setTarget(string $target): self
40 | {
41 | $this->target = $target;
42 |
43 | return $this;
44 | }
45 |
46 | public function toArray(): array
47 | {
48 | return [
49 | '@type' => ActionInterface::HTTP_POST_ACTION,
50 | 'name' => $this->getName(),
51 | 'target' => $this->getTarget(),
52 | ];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "skrepr/teams-connector",
3 | "description": "A simple PHP package for sending messages to Microsoft Teams",
4 | "type": "library",
5 | "keywords": ["Teams", "webhook", "Microsoft", "MS Teams", "PHP", "Card", "Section", "ActionCard"],
6 | "license": "MIT",
7 | "require": {
8 | "php": "^7.4 || ^8.0",
9 | "ext-json": "*",
10 | "php-http/discovery": "^1.15",
11 | "psr/http-client-implementation": "^1.0",
12 | "psr/http-factory-implementation": "^1.0"
13 | },
14 | "require-dev": {
15 | "phpunit/phpunit": "^9.0",
16 | "guzzlehttp/guzzle": "^7.3"
17 | },
18 | "authors": [
19 | {
20 | "name": "Evert Jan Hakvoort",
21 | "email": "evertjan@hakvoort.io"
22 | }
23 | ],
24 | "autoload": {
25 | "psr-4": {
26 | "Skrepr\\TeamsConnector\\": "src/"
27 | }
28 | },
29 | "autoload-dev": {
30 | "psr-4": {
31 | "Skrepr\\TeamsConnector\\Tests\\": "tests/"
32 | }
33 | },
34 | "config": {
35 | "sort-packages": true,
36 | "allow-plugins": {
37 | "php-http/discovery": false
38 | }
39 | },
40 | "scripts": {
41 | "test": "vendor/bin/phpunit"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/Actions/OpenUriActionUnitTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(OpenUriAction::class, $this->getAction());
16 | }
17 |
18 | public function testSetName(): void
19 | {
20 | $action = $this->getAction();
21 |
22 | $action->setName('Hello world');
23 |
24 | $this->assertSame('Hello world', $action->getName());
25 | }
26 |
27 | public function testSetTarget(): void
28 | {
29 | $action = $this->getAction();
30 |
31 | $action->setTarget('https://test.com');
32 |
33 | $this->assertSame('https://test.com', $action->getTarget());
34 | }
35 |
36 | public function testToArray(): void
37 | {
38 | $action = $this->getAction();
39 |
40 | $output = [
41 | '@type' => ActionInterface::OPEN_URI_ACTION,
42 | 'name' => 'name',
43 | 'target' => 'https://...',
44 | ];
45 |
46 | $this->assertSame($output, $action->toArray());
47 | }
48 |
49 | protected function getAction(): OpenUriAction
50 | {
51 | return new OpenUriAction('name', 'https://...');
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Actions/HttpPostActionUnitTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(HttpPostAction::class, $this->getAction());
16 | }
17 |
18 | public function testSetName(): void
19 | {
20 | $action = $this->getAction();
21 |
22 | $action->setName('Hello world');
23 |
24 | $this->assertSame('Hello world', $action->getName());
25 | }
26 |
27 | public function testSetTarget(): void
28 | {
29 | $action = $this->getAction();
30 |
31 | $action->setTarget('https://test.com');
32 |
33 | $this->assertSame('https://test.com', $action->getTarget());
34 | }
35 |
36 | public function testToArray(): void
37 | {
38 | $action = $this->getAction();
39 |
40 | $output = [
41 | '@type' => ActionInterface::HTTP_POST_ACTION,
42 | 'name' => 'name',
43 | 'target' => 'https://...',
44 | ];
45 |
46 | $this->assertSame($output, $action->toArray());
47 | }
48 |
49 | protected function getAction(): HttpPostAction
50 | {
51 | return new HttpPostAction('name', 'https://...');
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Inputs/DateInputUnitTest.php:
--------------------------------------------------------------------------------
1 | getInput();
16 |
17 | $this->assertSame('dueDate', $input->getId());
18 | $this->assertSame('Enter a due date for this task', $input->getTitle());
19 | }
20 |
21 | public function testSetId(): void
22 | {
23 | $input = $this->getInput();
24 |
25 | $input->setId('newId');
26 |
27 | $this->assertSame('newId', $input->getId());
28 | }
29 |
30 | public function setTitle(): void
31 | {
32 | $input = $this->getInput();
33 |
34 | $input->setTitle('title');
35 |
36 | $this->assertSame('title', $input->getTitle());
37 | }
38 |
39 | public function testToArray(): void
40 | {
41 | $input = $this->getInput();
42 |
43 | $output = [
44 | '@type' => InputInterface::DATE_INPUT,
45 | 'id' => 'dueDate',
46 | 'title' => 'Enter a due date for this task',
47 | ];
48 |
49 | $this->assertSame($output, $input->toArray());
50 | }
51 |
52 | public function getInput(): DateInput
53 | {
54 | return new DateInput('dueDate', 'Enter a due date for this task');
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Inputs/TextInput.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class TextInput implements InputInterface
11 | {
12 | private string $id;
13 |
14 | private string $title;
15 |
16 | private bool $multiline;
17 |
18 | public function __construct(string $id, string $title, bool $isMultiline = false)
19 | {
20 | $this->id = $id;
21 | $this->title = $title;
22 | $this->multiline = $isMultiline;
23 | }
24 |
25 | public function getId(): string
26 | {
27 | return $this->id;
28 | }
29 |
30 | public function setId(string $id): self
31 | {
32 | $this->id = $id;
33 |
34 | return $this;
35 | }
36 |
37 | public function getTitle(): string
38 | {
39 | return $this->title;
40 | }
41 |
42 | public function setTitle(string $title): self
43 | {
44 | $this->title = $title;
45 |
46 | return $this;
47 | }
48 |
49 | public function isMultiline(): bool
50 | {
51 | return $this->multiline;
52 | }
53 |
54 | public function setMultiline(bool $multiline): self
55 | {
56 | $this->multiline = $multiline;
57 |
58 | return $this;
59 | }
60 |
61 | public function toArray(): array
62 | {
63 | return [
64 | '@type' => InputInterface::TEXT_INPUT,
65 | 'id' => $this->getId(),
66 | 'isMultiline' => $this->isMultiline(),
67 | 'title' => $this->getTitle(),
68 | ];
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/RequestBuilder.php:
--------------------------------------------------------------------------------
1 |
15 | *
16 | * @internal
17 | */
18 | final class RequestBuilder
19 | {
20 | private RequestFactoryInterface $requestFactory;
21 |
22 | private StreamFactoryInterface $streamFactory;
23 |
24 | public function __construct(
25 | RequestFactoryInterface $requestFactory = null,
26 | StreamFactoryInterface $streamFactory = null
27 | ) {
28 | $this->streamFactory = $streamFactory ?? new Psr17Factory();
29 | $this->requestFactory = $requestFactory ?? ($this->streamFactory instanceof RequestFactoryInterface ? $this->streamFactory : new Psr17Factory());
30 | }
31 |
32 | /**
33 | * Creates a new PSR-7 request.
34 | *
35 | * @param array $headers
36 | * @param StreamInterface|string|null $body
37 | */
38 | public function create(string $method, string $uri, array $headers = [], $body = null): RequestInterface
39 | {
40 | $request = $this->requestFactory->createRequest($method, $uri);
41 |
42 | foreach ($headers as $name => $value) {
43 | $request = $request->withHeader($name, $value);
44 | }
45 |
46 | if ($body !== null) {
47 | if (!$body instanceof StreamInterface) {
48 | $body = $this->streamFactory->createStream($body);
49 | }
50 |
51 | $request = $request->withBody($body);
52 | }
53 |
54 | return $request;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tests/Inputs/TextInputUnitTest.php:
--------------------------------------------------------------------------------
1 | getInput();
16 |
17 | $this->assertSame('comment', $input->getId());
18 | $this->assertSame('Add a comment here for this task', $input->getTitle());
19 | $this->assertTrue($input->isMultiline());
20 | }
21 |
22 | public function testSetId(): void
23 | {
24 | $input = $this->getInput();
25 |
26 | $input->setId('newId');
27 |
28 | $this->assertSame('newId', $input->getId());
29 | }
30 |
31 | public function testSetTitle(): void
32 | {
33 | $input = $this->getInput();
34 |
35 | $input->setTitle('title');
36 |
37 | $this->assertSame('title', $input->getTitle());
38 | }
39 |
40 | public function testSetMultiLine(): void
41 | {
42 | $input = $this->getInput();
43 |
44 | $input->setMultiline(true);
45 | $this->assertTrue($input->isMultiline());
46 |
47 | $input->setMultiline(false);
48 | $this->assertFalse($input->isMultiline());
49 | }
50 |
51 | public function testToArray(): void
52 | {
53 | $input = $this->getInput();
54 |
55 | $output = [
56 | '@type' => InputInterface::TEXT_INPUT,
57 | 'id' => 'comment',
58 | 'isMultiline' => true,
59 | 'title' => 'Add a comment here for this task',
60 | ];
61 |
62 | $this->assertSame($output, $input->toArray());
63 | }
64 |
65 | public function getInput(): TextInput
66 | {
67 | return new TextInput('comment', 'Add a comment here for this task', true);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Client.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | final class Client
18 | {
19 | private string $endPoint;
20 |
21 | private ClientInterface $client;
22 |
23 | private RequestBuilder $requestBuilder;
24 |
25 | public function __construct(
26 | string $endPoint,
27 | ClientInterface $client = null,
28 | RequestBuilder $requestBuilder = null
29 | ) {
30 | $this->endPoint = $endPoint;
31 | $this->client = $client ?? Psr18ClientDiscovery::find();
32 | $this->requestBuilder = $requestBuilder ?? new RequestBuilder();
33 | }
34 |
35 | public function getEndPoint(): string
36 | {
37 | return $this->endPoint;
38 | }
39 |
40 | /**
41 | * @throws ClientExceptionInterface
42 | */
43 | public function send(CardInterface $card): void
44 | {
45 | $request = $this->createRequest($card);
46 |
47 | $response = $this->client->sendRequest($request);
48 |
49 | $statusCode = $response->getStatusCode();
50 |
51 | if ($statusCode === 401 || $statusCode === 403) {
52 | throw new InvalidCredentials();
53 | } elseif ($statusCode >= 300) {
54 | throw new InvalidServerResponse((string) $response->getBody(), $statusCode);
55 | }
56 |
57 | if ($response->getBody()->getContents() !== '1') {
58 | throw new InvalidServerResponse('Something went wrong!');
59 | }
60 | }
61 |
62 | private function createRequest(CardInterface $card): RequestInterface
63 | {
64 | $content = json_encode($card->toArray());
65 |
66 | return $this->requestBuilder->create(
67 | 'POST',
68 | $this->endPoint,
69 | ['Content-Type' => 'application/json', 'Content-Length' => strlen($content)],
70 | $content
71 | );
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Inputs/MultiChoiceInput.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class MultiChoiceInput implements InputInterface
11 | {
12 | private string $id;
13 |
14 | private string $title;
15 |
16 | private bool $multiSelect;
17 |
18 | private array $choices;
19 |
20 | public function __construct(string $id, string $title, bool $isMultiSelect = false)
21 | {
22 | $this->id = $id;
23 | $this->title = $title;
24 | $this->multiSelect = $isMultiSelect;
25 | $this->choices = [];
26 | }
27 |
28 | public function getId(): string
29 | {
30 | return $this->id;
31 | }
32 |
33 | public function setId(string $id): self
34 | {
35 | $this->id = $id;
36 |
37 | return $this;
38 | }
39 |
40 | public function getTitle(): string
41 | {
42 | return $this->title;
43 | }
44 |
45 | public function setTitle(string $title): self
46 | {
47 | $this->title = $title;
48 |
49 | return $this;
50 | }
51 |
52 | public function getChoices(): array
53 | {
54 | return $this->choices;
55 | }
56 |
57 | public function isMultiSelect(): bool
58 | {
59 | return $this->multiSelect;
60 | }
61 |
62 | public function setMultiSelect(bool $multiSelect): self
63 | {
64 | $this->multiSelect = $multiSelect;
65 |
66 | return $this;
67 | }
68 |
69 | public function addChoice(string $display, string $value): self
70 | {
71 | $this->choices[] = [
72 | 'display' => $display,
73 | 'value' => $value,
74 | ];
75 |
76 | return $this;
77 | }
78 |
79 | public function clearChoices(): self
80 | {
81 | $this->choices = [];
82 |
83 | return $this;
84 | }
85 |
86 | public function toArray(): array
87 | {
88 | return [
89 | '@type' => InputInterface::MULTI_CHOICE_INPUT,
90 | 'id' => $this->getId(),
91 | 'title' => $this->getTitle(),
92 | 'isMultiSelect' => $this->isMultiSelect(),
93 | 'choices' => $this->getChoices(),
94 | ];
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Actions/ActionCard.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | final class ActionCard implements ActionInterface
13 | {
14 | private string $name;
15 |
16 | /**
17 | * @var InputInterface[]
18 | */
19 | private array $inputs;
20 |
21 | /**
22 | * @var ActionInterface[]
23 | */
24 | private array $actions;
25 |
26 | public function __construct(string $name)
27 | {
28 | $this->name = $name;
29 | $this->inputs = [];
30 | $this->actions = [];
31 | }
32 |
33 | public function getName(): string
34 | {
35 | return $this->name;
36 | }
37 |
38 | public function setName(string $name): self
39 | {
40 | $this->name = $name;
41 |
42 | return $this;
43 | }
44 |
45 | /**
46 | * @return ActionInterface[]
47 | */
48 | public function getActions(): array
49 | {
50 | return $this->actions;
51 | }
52 |
53 | /**
54 | * @return InputInterface[]
55 | */
56 | public function getInputs(): array
57 | {
58 | return $this->inputs;
59 | }
60 |
61 | public function addInput(InputInterface $input): self
62 | {
63 | $this->inputs[] = $input;
64 |
65 | return $this;
66 | }
67 |
68 | public function clearInputs(): self
69 | {
70 | $this->inputs = [];
71 |
72 | return $this;
73 | }
74 |
75 | public function addAction(ActionInterface $action): self
76 | {
77 | $this->actions[] = $action;
78 |
79 | return $this;
80 | }
81 |
82 | public function clearActions(): self
83 | {
84 | $this->actions = [];
85 |
86 | return $this;
87 | }
88 |
89 | public function toArray(): array
90 | {
91 | return [
92 | '@type' => ActionInterface::ACTION_CARD,
93 | 'name' => $this->getName(),
94 | 'inputs' => array_map(static fn (InputInterface $input) => $input->toArray(), $this->getInputs()),
95 | 'actions' => array_map(static fn (ActionInterface $action) => $action->toArray(), $this->getActions()),
96 | ];
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/tests/Inputs/MultiChoiceInputUnitTest.php:
--------------------------------------------------------------------------------
1 | getInput();
16 |
17 | $this->assertSame('list', $input->getId());
18 | $this->assertSame('Select a status', $input->getTitle());
19 | $this->assertTrue($input->isMultiSelect());
20 | }
21 |
22 | public function testSetId(): void
23 | {
24 | $input = $this->getInput();
25 |
26 | $input->setId('newId');
27 |
28 | $this->assertSame('newId', $input->getId());
29 | }
30 |
31 | public function setTitle(): void
32 | {
33 | $input = $this->getInput();
34 |
35 | $input->setTitle('title');
36 |
37 | $this->assertSame('title', $input->getTitle());
38 | }
39 |
40 | public function testAddChoices(): void
41 | {
42 | $input = $this->getInput();
43 |
44 | $input->addChoice('In Progress', '2');
45 | $input->addChoice('Done', '3');
46 |
47 | $output = [
48 | [
49 | 'display' => 'In Progress',
50 | 'value' => '2',
51 | ],
52 | [
53 | 'display' => 'Done',
54 | 'value' => '3',
55 | ],
56 | ];
57 |
58 | $this->assertSame($output, $input->getChoices());
59 | }
60 |
61 | public function testSetMultiSelect(): void
62 | {
63 | $input = $this->getInput();
64 |
65 | $input->setMultiSelect(true);
66 | $this->assertTrue($input->isMultiSelect());
67 |
68 | $input->setMultiSelect(false);
69 | $this->assertFalse($input->isMultiSelect());
70 | }
71 |
72 | public function testClearChoices(): void
73 | {
74 | $input = $this->getInput();
75 |
76 | $input->addChoice('foo', 'bar');
77 | $this->assertGreaterThanOrEqual(1, count($input->getChoices()));
78 |
79 | $input->clearChoices();
80 | $this->assertSame([], $input->getChoices());
81 | }
82 |
83 | public function testToArray(): void
84 | {
85 | $input = $this->getInput();
86 |
87 | $output = [
88 | '@type' => InputInterface::MULTI_CHOICE_INPUT,
89 | 'id' => 'list',
90 | 'title' => 'Select a status',
91 | 'isMultiSelect' => true,
92 | 'choices' => [],
93 | ];
94 |
95 | $this->assertSame($output, $input->toArray());
96 | }
97 |
98 | protected function getInput(): MultiChoiceInput
99 | {
100 | return new MultiChoiceInput('list', 'Select a status', true);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are **welcome** and will be fully **credited**.
4 |
5 | Please read and understand the contribution guide before creating an issue or pull request.
6 |
7 | ## Etiquette
8 |
9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code
10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be
11 | extremely unfair for them to suffer abuse or anger for their hard work.
12 |
13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the
14 | world that developers are civilized and selfless people.
15 |
16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient
17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.
18 |
19 | ## Viability
20 |
21 | When requesting or submitting new features, first consider whether it might be useful to others. Open
22 | source projects are used by many developers, who may have entirely different needs to your own. Think about whether your feature is likely to be used by other users of the project.
23 |
24 | ## Procedure
25 |
26 | Before filing an issue:
27 |
28 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.
29 | - Check to make sure your feature suggestion isn't already present within the project.
30 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress.
31 | - Check the pull requests tab to ensure that the feature isn't already in progress.
32 |
33 | Before submitting a pull request:
34 |
35 | - Check the codebase to ensure that your feature doesn't already exist.
36 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix.
37 |
38 | ## Requirements
39 |
40 | If the project maintainer has any additional requirements, you will find them listed here.
41 |
42 | - **[PSR-122 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer).
43 |
44 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests.
45 |
46 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
47 |
48 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option.
49 |
50 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
51 |
52 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
53 |
--------------------------------------------------------------------------------
/tests/CardUnitTest.php:
--------------------------------------------------------------------------------
1 | getCard();
19 |
20 | $this->assertSame('Title', $card->getTitle());
21 |
22 | $this->assertNull($card->getText());
23 | }
24 |
25 | public function testSetText(): void
26 | {
27 | $card = $this->getCard();
28 |
29 | $card->setText('new Text');
30 |
31 | $this->assertSame('new Text', $card->getText());
32 | }
33 |
34 | public function testSetTitle(): void
35 | {
36 | $card = $this->getCard();
37 |
38 | $card->setTitle('new Title');
39 |
40 | $this->assertSame('new Title', $card->getTitle());
41 | }
42 |
43 | public function testSetThemeColor(): void
44 | {
45 | $card = $this->getCard();
46 |
47 | $card->setThemeColor(CardInterface::STATUS_SUCCESS);
48 |
49 | $this->assertSame(CardInterface::STATUS_SUCCESS, $card->getThemeColor());
50 | }
51 |
52 | public function testValidateThemeColor(): void
53 | {
54 | $card = $this->getCard();
55 |
56 | $this->expectException(InvalidArgumentException::class);
57 | $card->setThemeColor('invalid');
58 | }
59 |
60 | public function testAddSection(): void
61 | {
62 | $card = $this->getCard();
63 |
64 | $section = new Section('Test');
65 |
66 | $card->addSection($section);
67 |
68 | $this->assertSame($section, $card->getSections()[0]);
69 | }
70 |
71 | public function testAddingPotentialAction(): void
72 | {
73 | $card = $this->getCard();
74 |
75 | $action = new HttpPostAction('Test', 'https://...');
76 |
77 | $card->addPotentialAction($action);
78 |
79 | $this->assertSame($action, $card->getPotentialActions()[0]);
80 | }
81 |
82 | public function testToArray(): void
83 | {
84 | $card = (new Card('Larry Bryant created a new task'))
85 | ->setText('Yes, he did')
86 | ->setThemeColor(CardInterface::STATUS_DEFAULT)
87 | ->setTitle('Adding Title to the card');
88 |
89 | $expectedData = [
90 | '@type' => 'MessageCard',
91 | 'title' => 'Adding Title to the card',
92 | 'themeColor' => CardInterface::STATUS_DEFAULT,
93 | 'text' => 'Yes, he did',
94 | 'sections' => [],
95 | 'potentialAction' => [],
96 | ];
97 |
98 | $this->assertSame($expectedData, $card->toArray());
99 | }
100 |
101 | protected function getCard(): Card
102 | {
103 | return new Card('Title');
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Section/Section.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class Section implements SectionInterface
11 | {
12 | private string $activityTitle;
13 |
14 | private ?string $activitySubtitle;
15 |
16 | private ?string $activityText;
17 |
18 | private ?string $activityImage;
19 |
20 | private bool $markDown;
21 |
22 | /**
23 | * @var array>
24 | */
25 | private array $facts;
26 |
27 | public function __construct(string $activityTitle)
28 | {
29 | $this->activityTitle = $activityTitle;
30 | $this->activityImage = null;
31 | $this->activitySubtitle = null;
32 | $this->activityText = null;
33 | $this->markDown = true;
34 | $this->facts = [];
35 | }
36 |
37 | public function getActivityTitle(): string
38 | {
39 | return $this->activityTitle;
40 | }
41 |
42 | public function setActivityTitle(string $activityTitle): self
43 | {
44 | $this->activityTitle = $activityTitle;
45 |
46 | return $this;
47 | }
48 |
49 | public function getActivityImage(): ?string
50 | {
51 | return $this->activityImage;
52 | }
53 |
54 | public function setActivityImage(?string $activityImage): self
55 | {
56 | $this->activityImage = $activityImage;
57 |
58 | return $this;
59 | }
60 |
61 | public function getActivitySubtitle(): string
62 | {
63 | return $this->activitySubtitle;
64 | }
65 |
66 | public function setActivitySubtitle(string $activitySubtitle): self
67 | {
68 | $this->activitySubtitle = $activitySubtitle;
69 |
70 | return $this;
71 | }
72 |
73 | public function getActivityText(): ?string
74 | {
75 | return $this->activityText;
76 | }
77 |
78 | public function setActivityText(?string $activityText): self
79 | {
80 | $this->activityText = $activityText;
81 |
82 | return $this;
83 | }
84 |
85 | public function isMarkdown(): bool
86 | {
87 | return $this->markDown;
88 | }
89 |
90 | public function setMarkDown(bool $markdown): self
91 | {
92 | $this->markDown = $markdown;
93 |
94 | return $this;
95 | }
96 |
97 | public function getFacts(): array
98 | {
99 | return $this->facts;
100 | }
101 |
102 | public function addFact(string $name, string $value): self
103 | {
104 | $this->facts[] = [
105 | 'name' => $name,
106 | 'value' => $value,
107 | ];
108 |
109 | return $this;
110 | }
111 |
112 | public function clearFacts(): self
113 | {
114 | $this->facts = [];
115 |
116 | return $this;
117 | }
118 |
119 | public function toArray(): array
120 | {
121 | return [
122 | 'activityTitle' => $this->activityTitle,
123 | 'activitySubtitle' => $this->activitySubtitle,
124 | 'activityText' => $this->activityText,
125 | 'activityImage' => $this->activityImage,
126 | 'markdown' => $this->markDown,
127 | 'facts' => $this->facts,
128 | ];
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/Card.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | final class Card implements CardInterface
15 | {
16 | private string $themeColor;
17 |
18 | private string $title;
19 |
20 | private ?string $text;
21 |
22 | /**
23 | * @var SectionInterface[]
24 | */
25 | private array $sections;
26 |
27 | /**
28 | * @var ActionInterface[]
29 | */
30 | private array $potentialAction;
31 |
32 | public function __construct(string $title)
33 | {
34 | $this->title = $title;
35 | $this->text = null;
36 | $this->sections = [];
37 | $this->potentialAction = [];
38 | $this->setThemeColor(CardInterface::STATUS_DEFAULT);
39 | }
40 |
41 | public function getTitle(): string
42 | {
43 | return $this->title;
44 | }
45 |
46 | public function setTitle(string $title): self
47 | {
48 | $this->title = $title;
49 |
50 | return $this;
51 | }
52 |
53 | public function getThemeColor(): string
54 | {
55 | return $this->themeColor;
56 | }
57 |
58 | public function setThemeColor(string $themeColor): self
59 | {
60 | $this->validateThemeColor($themeColor);
61 |
62 | $this->themeColor = $themeColor;
63 |
64 | return $this;
65 | }
66 |
67 | public function getText(): ?string
68 | {
69 | return $this->text;
70 | }
71 |
72 | public function setText(string $text): self
73 | {
74 | $this->text = $text;
75 |
76 | return $this;
77 | }
78 |
79 | /**
80 | * @return SectionInterface[]
81 | */
82 | public function getSections(): array
83 | {
84 | return $this->sections;
85 | }
86 |
87 | public function addSection(SectionInterface $section): self
88 | {
89 | $this->sections[] = $section;
90 |
91 | return $this;
92 | }
93 |
94 | /**
95 | * @return ActionInterface[]
96 | */
97 | public function getPotentialActions(): array
98 | {
99 | return $this->potentialAction;
100 | }
101 |
102 | public function addPotentialAction(ActionInterface $action): self
103 | {
104 | $this->potentialAction[] = $action;
105 |
106 | return $this;
107 | }
108 |
109 | public function toArray(): array
110 | {
111 | return [
112 | '@type' => 'MessageCard',
113 | 'title' => $this->title,
114 | 'themeColor' => $this->themeColor,
115 | 'text' => $this->text,
116 | 'sections' => array_map(static fn (SectionInterface $section) => $section->toArray(), $this->sections),
117 | 'potentialAction' => array_map(static fn (ActionInterface $action) => $action->toArray(), $this->potentialAction),
118 | ];
119 | }
120 |
121 | private function validateThemeColor(string $themeColor): void
122 | {
123 | if (!preg_match('/^#([0-9a-f]{6}|[0-9a-f]{3})$/i', $themeColor)) {
124 | throw new InvalidArgumentException('MessageCard themeColor must have a valid hex color format.');
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/tests/SectionUnitTest.php:
--------------------------------------------------------------------------------
1 | getSection();
15 |
16 | $this->assertSame('Section title', $section->getActivityTitle());
17 | }
18 |
19 | public function testSetActivityTitle(): void
20 | {
21 | $section = $this->getSection();
22 |
23 | $section->setActivityTitle('Setting section title');
24 |
25 | $this->assertSame('Setting section title', $section->getActivityTitle());
26 | }
27 |
28 | public function testSetSubTitle(): void
29 | {
30 | $section = $this->getSection();
31 |
32 | $section->setActivitySubtitle('Setting section subtitle');
33 |
34 | $this->assertSame('Setting section subtitle', $section->getActivitySubtitle());
35 | }
36 |
37 | public function testSetText(): void
38 | {
39 | $section = $this->getSection();
40 |
41 | $section->setActivityText('Adding a new text');
42 |
43 | $this->assertSame('Adding a new text', $section->getActivityText());
44 | }
45 |
46 | public function testSetActivityImage(): void
47 | {
48 | $section = $this->getSection();
49 |
50 | $section->setActivityImage('https://teamsnodesample.azurewebsites.net/static/img/image5.png');
51 |
52 | $this->assertSame('https://teamsnodesample.azurewebsites.net/static/img/image5.png', $section->getActivityImage());
53 | }
54 |
55 | public function testSetMarkDown(): void
56 | {
57 | $section = $this->getSection();
58 |
59 | $section->setMarkDown(true);
60 | $this->assertTrue($section->isMarkdown());
61 |
62 | $section->setMarkDown(false);
63 | $this->assertFalse($section->isMarkdown());
64 | }
65 |
66 | public function testAddFact(): void
67 | {
68 | $section = $this->getSection();
69 |
70 | $section->addFact('DueDate', 'Tomorrow');
71 |
72 | $this->assertSame([
73 | 'name' => 'DueDate',
74 | 'value' => 'Tomorrow',
75 | ], $section->getFacts()[0]);
76 | }
77 |
78 | public function testClearFacts(): void
79 | {
80 | $section = $this->getSection();
81 |
82 | $section->addFact('DueDate', 'Tomorrow');
83 |
84 | $section->clearFacts();
85 |
86 | $this->assertSame([], $section->getFacts());
87 | }
88 |
89 | public function testToArray(): void
90 | {
91 | $section = $this->getSection();
92 |
93 | $section->addFact('DueDate', 'Tomorrow')
94 | ->setActivityImage('https://teamsnodesample.azurewebsites.net/static/img/image5.png')
95 | ->setActivityText('Adding a new text')
96 | ->setActivitySubtitle('Adding a subtitle')
97 | ->setMarkDown(false);
98 |
99 | $expectedData = [
100 | 'activityTitle' => 'Section title',
101 | 'activitySubtitle' => 'Adding a subtitle',
102 | 'activityText' => 'Adding a new text',
103 | 'activityImage' => 'https://teamsnodesample.azurewebsites.net/static/img/image5.png',
104 | 'markdown' => false,
105 | 'facts' => [
106 | [
107 | 'name' => 'DueDate',
108 | 'value' => 'Tomorrow',
109 | ],
110 | ],
111 | ];
112 |
113 | $this->assertSame($expectedData, $section->toArray());
114 | }
115 |
116 | protected function getSection(): Section
117 | {
118 | return new Section('Section title');
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/tests/Actions/ActionCardUnitTest.php:
--------------------------------------------------------------------------------
1 | getAction();
19 |
20 | $this->assertSame('Add a comment', $action->getName());
21 | }
22 |
23 | public function testSetNameWithSetter(): void
24 | {
25 | $action = $this->getAction();
26 |
27 | $action->setName('Hello world');
28 |
29 | $this->assertSame('Hello world', $action->getName());
30 | }
31 |
32 | public function testAddAction(): void
33 | {
34 | $action = new HttpPostAction('HttpPostAction', 'https://...');
35 |
36 | $actionCard = $this->getAction();
37 |
38 | $actionCard->addAction($action);
39 |
40 | $this->assertSame($action, $actionCard->getActions()[0]);
41 | }
42 |
43 | public function testClearActions(): void
44 | {
45 | $action = new HttpPostAction('HttpPostAction', 'https://...');
46 |
47 | $actionCard = $this->getAction();
48 |
49 | $actionCard->addAction($action);
50 |
51 | $this->assertSame($action, $actionCard->getActions()[0]);
52 |
53 | $actionCard->clearActions();
54 |
55 | $this->assertSame([], $actionCard->getActions());
56 | }
57 |
58 | public function testAddInput(): void
59 | {
60 | $actionCard = $this->getAction();
61 |
62 | $input = new TextInput('Test input', 'Add title');
63 |
64 | $actionCard->addInput($input);
65 |
66 | $this->assertSame($input, $actionCard->getInputs()[0]);
67 | }
68 |
69 | public function testClearInputs(): void
70 | {
71 | $actionCard = $this->getAction();
72 |
73 | $input = new TextInput('Test input', 'Add title');
74 |
75 | $actionCard->addInput($input);
76 |
77 | $this->assertSame($input, $actionCard->getInputs()[0]);
78 |
79 | $actionCard->clearInputs();
80 |
81 | $this->assertSame([], $actionCard->getInputs());
82 | }
83 |
84 | public function testToArray(): void
85 | {
86 | $actionCard = $this->getAction()
87 | ->addInput(new TextInput('comment', 'Add a comment here for this task'))
88 | ->addInput(new DateInput('dueDate', 'Enter a due date for this task'))
89 | ->addInput((new MultiChoiceInput('list', 'Select a status', true))->addChoice('In Progress', '2'))
90 | ->addAction(new HttpPostAction('Add comment', 'http://...'));
91 |
92 | $actionOutput = [
93 | '@type' => 'ActionCard',
94 | 'name' => 'Add a comment',
95 | 'inputs' => [
96 | [
97 | '@type' => 'TextInput',
98 | 'id' => 'comment',
99 | 'isMultiline' => false,
100 | 'title' => 'Add a comment here for this task',
101 | ],
102 | [
103 | '@type' => 'DateInput',
104 | 'id' => 'dueDate',
105 | 'title' => 'Enter a due date for this task',
106 | ],
107 | [
108 | '@type' => 'MultichoiceInput',
109 | 'id' => 'list',
110 | 'title' => 'Select a status',
111 | 'isMultiSelect' => true,
112 | 'choices' => [
113 | [
114 | 'display' => 'In Progress',
115 | 'value' => '2',
116 | ],
117 | ],
118 | ],
119 | ],
120 | 'actions' => [
121 | [
122 | '@type' => 'HttpPOST',
123 | 'name' => 'Add comment',
124 | 'target' => 'http://...',
125 | ],
126 | ],
127 | ];
128 |
129 | $this->assertSame($actionCard->toArray(), $actionOutput);
130 | }
131 |
132 | protected function getAction(): ActionCard
133 | {
134 | return new ActionCard('Add a comment');
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Teams connector
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | This package allows you to send notifications to Microsoft Teams.
18 |
19 | ## Installation
20 |
21 | You can install the package using the [Composer](https://getcomposer.org/) package manager. You can install it by running this command in your project root:
22 |
23 | ```sh
24 | composer require skrepr/teams-connector
25 | ```
26 |
27 | Then [create an incoming webhook](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook) on your Microsoft teams channel for the package to use.
28 |
29 | ## Basic Usage
30 |
31 | ### Create a simple card
32 |
33 | ```php
34 | setText('Yes, he did')
48 | ->setThemeColor(CardInterface::STATUS_DEFAULT)
49 | ->setTitle('Adding Title to the card');
50 |
51 | $teamsClient->send($card);
52 | ```
53 |
54 | ### Adding a section
55 | ```php
56 | setActivitySubtitle('On Project Tango')
72 | ->setActivityImage('https://teamsnodesample.azurewebsites.net/static/img/image5.png')
73 | ->addFact('Assigned to', 'Unassigned')
74 | ->addFact('Due date', 'Mon May 01 2017 17:07:18 GMT-0700 (Pacific Daylight Time)');
75 |
76 | $card->addSection($section);
77 |
78 | $teamsClient->send($card);
79 | ```
80 |
81 | ### Adding actions and inputs to the card
82 | ```php
83 | setText('Yes, he did');
99 |
100 | $actionCard = (new ActionCard('Add a comment'))
101 | ->addInput(new TextInput('comment', 'Add a comment here for this task'))
102 | ->addAction(new HttpPostAction('Add comment', 'http://...'));
103 |
104 | $card->addPotentialAction($actionCard);
105 |
106 | $teamsClient->send($card);
107 | ```
108 |
109 | ### HTTP Clients
110 | In order to talk to Microsoft Teams API, you need an HTTP adapter. We rely on HTTPlug
111 | which defines how HTTP message should be sent and received. You can use any library to send HTTP messages
112 | that implements [php-http/client-implementation](https://packagist.org/providers/php-http/client-implementation).
113 |
114 | ## Testing
115 | ``` bash
116 | composer test
117 | ```
118 |
119 | ## Credits
120 | - [Skrepr](https://skrepr.com)
121 | - [Evert Jan Hakvoort](https://github.com/EJTJ3)
122 | - [Jon Mulder](https://github.com/jonmldr)
123 | - [All Contributors](../../contributors)
124 |
125 | ## Security Vulnerabilities
126 |
127 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
128 |
129 | ## License
130 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
131 |
--------------------------------------------------------------------------------
/tests/ClientFunctionalTest.php:
--------------------------------------------------------------------------------
1 | 'MessageCard',
25 | 'title' => 'Text',
26 | 'themeColor' => CardInterface::STATUS_DEFAULT,
27 | 'text' => null,
28 | 'sections' => [],
29 | 'potentialAction' => [],
30 | ];
31 |
32 | $this->assertSame($expectedHttpData, $card->toArray());
33 | }
34 |
35 | public function testSimpleCardWithSection(): void
36 | {
37 | $section = (new Section('Section'))
38 | ->setActivityText('Dummy text')
39 | ->setActivitySubtitle('Subtitle')
40 | ->setActivityImage('https://teamsnodesample.azurewebsites.net/static/img/image5.png')
41 | ->addFact('Fact 1', 'Value 1')
42 | ->addFact('Fact 2', 'Value 2');
43 |
44 | $card = (new Card(''))
45 | ->setTitle('Title')
46 | ->setText('Text')
47 | ->setThemeColor(CardInterface::STATUS_FAILURE)
48 | ->addSection($section);
49 |
50 | $sectionsOutput = [
51 | 'activityTitle' => 'Section',
52 | 'activitySubtitle' => 'Subtitle',
53 | 'activityText' => 'Dummy text',
54 | 'activityImage' => 'https://teamsnodesample.azurewebsites.net/static/img/image5.png',
55 | 'markdown' => true,
56 | 'facts' => [
57 | [
58 | 'name' => 'Fact 1',
59 | 'value' => 'Value 1',
60 | ],
61 | [
62 | 'name' => 'Fact 2',
63 | 'value' => 'Value 2',
64 | ],
65 | ],
66 | ];
67 |
68 | $expectedHttpData = [
69 | '@type' => 'MessageCard',
70 | 'title' => 'Title',
71 | 'themeColor' => CardInterface::STATUS_FAILURE,
72 | 'text' => 'Text',
73 | 'sections' => [$sectionsOutput],
74 | 'potentialAction' => [],
75 | ];
76 |
77 | $this->assertSame($expectedHttpData, $card->toArray());
78 | }
79 |
80 | public function testCardWithSectionAndActions(): void
81 | {
82 | $actionCard = (new ActionCard('Add a comment'))
83 | ->addInput(new TextInput('comment', 'Add a comment here for this task'))
84 | ->addInput(new DateInput('dueDate', 'Enter a due date for this task'))
85 | ->addInput((new MultiChoiceInput('list', 'Select a status', true))->addChoice('In Progress', '2'))
86 | ->addAction(new HttpPostAction('Add comment', 'http://...'));
87 |
88 | $section = (new Section('Larry Bryant created a new task'))
89 | ->setActivitySubtitle('On Project Tango')
90 | ->setActivityText('Dummy text')
91 | ->setActivityImage('https://teamsnodesample.azurewebsites.net/static/img/image5.png')
92 | ->addFact('Assigned to', 'Unassigned')
93 | ->addFact('Due date', 'Mon May 01 2017 17:07:18 GMT-0700 (Pacific Daylight Time)')
94 | ->setMarkDown(false);
95 |
96 | $card = (new Card('Larry Bryant created a new task'))
97 | ->setText('Yes, he did')
98 | ->setThemeColor(CardInterface::STATUS_DEFAULT)
99 | ->setTitle('Adding Title to the card')
100 | ->addSection($section)
101 | ->addPotentialAction($actionCard);
102 |
103 | $actionOutput = [
104 | '@type' => 'ActionCard',
105 | 'name' => 'Add a comment',
106 | 'inputs' => [
107 | [
108 | '@type' => 'TextInput',
109 | 'id' => 'comment',
110 | 'isMultiline' => false,
111 | 'title' => 'Add a comment here for this task',
112 | ],
113 | [
114 | '@type' => 'DateInput',
115 | 'id' => 'dueDate',
116 | 'title' => 'Enter a due date for this task',
117 | ],
118 | [
119 | '@type' => 'MultichoiceInput',
120 | 'id' => 'list',
121 | 'title' => 'Select a status',
122 | 'isMultiSelect' => true,
123 | 'choices' => [
124 | [
125 | 'display' => 'In Progress',
126 | 'value' => '2',
127 | ],
128 | ],
129 | ],
130 | ],
131 | 'actions' => [
132 | [
133 | '@type' => 'HttpPOST',
134 | 'name' => 'Add comment',
135 | 'target' => 'http://...',
136 | ],
137 | ],
138 | ];
139 |
140 | $sectionsOutput = [
141 | 'activityTitle' => 'Larry Bryant created a new task',
142 | 'activitySubtitle' => 'On Project Tango',
143 | 'activityText' => 'Dummy text',
144 | 'activityImage' => 'https://teamsnodesample.azurewebsites.net/static/img/image5.png',
145 | 'markdown' => false,
146 | 'facts' => [
147 | [
148 | 'name' => 'Assigned to',
149 | 'value' => 'Unassigned',
150 | ],
151 | [
152 | 'name' => 'Due date',
153 | 'value' => 'Mon May 01 2017 17:07:18 GMT-0700 (Pacific Daylight Time)',
154 | ],
155 | ],
156 | ];
157 |
158 | $expectedHttpData = [
159 | '@type' => 'MessageCard',
160 | 'title' => 'Adding Title to the card',
161 | 'themeColor' => CardInterface::STATUS_DEFAULT,
162 | 'text' => 'Yes, he did',
163 | 'sections' => [$sectionsOutput],
164 | 'potentialAction' => [$actionOutput],
165 | ];
166 |
167 | $this->assertSame($expectedHttpData, $card->toArray());
168 | }
169 | }
170 |
--------------------------------------------------------------------------------