├── .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 | skrepr 4 |

5 |
6 |

Teams connector

7 |
8 | Releases 9 | LICENSE 10 | Issues 11 | PR 12 | Commits 13 | Stars 14 | Forks 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 | --------------------------------------------------------------------------------