diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a7c44dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9e9519b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml.dist export-ignore +/art export-ignore +/docs export-ignore +/tests export-ignore +/.editorconfig export-ignore +/.php_cs.dist.php export-ignore +/psalm.xml export-ignore +/psalm.xml.dist export-ignore +/testbench.yaml export-ignore +/UPGRADING.md export-ignore +/phpstan.neon.dist export-ignore +/phpstan-baseline.neon export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..b4ae1c4 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +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. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **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. + +**Happy coding**! diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..83b7546 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# These are supported funding model platforms +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [StanBarrows] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..81e7423 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/codebar-ag/laravel-zendesk/issues/new + about: Ask the community for help + - name: Request a feature + url: https://github.com/codebar-ag/laravel-zendesk/issues/new + about: Share ideas for new features + - name: Report a bug + url: https://github.com/codebar-ag/laravel-zendesk/issues/new + about: Report a reproducable bug diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..dd923a2 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +If you discover any security related issues, please email info@codebar.ch instead of using the issue tracker. diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..14da349 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,32 @@ +name: dependabot-auto-merge +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.3.3 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-merge Dependabot PRs for semver-minor updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge Dependabot PRs for semver-patch updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml new file mode 100644 index 0000000..3517cf1 --- /dev/null +++ b/.github/workflows/php-cs-fixer.yml @@ -0,0 +1,26 @@ +name: Check & fix styling + +on: + push: + branches: + - styling + +jobs: + php-cs-fixer: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + + - name: Run PHP CS Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --config=.php-cs-fixer.dist.php --allow-risky=yes + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Fix styling diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..900f541 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,26 @@ +name: PHPStan + +on: + push: + paths: + - '**.php' + - 'phpstan.neon.dist' + +jobs: + phpstan: + name: phpstan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + + - name: Install composer dependencies + uses: ramsey/composer-install@v2 + + - name: Run PHPStan + run: ./vendor/bin/phpstan --error-format=github diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..9f9a31c --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,61 @@ +name: run-tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + # os: [ ubuntu-latest, windows-latest ] + os: [ ubuntu-latest ] + php: [ 8.2 ] + laravel: [ 10.* ] + #stability: [ prefer-lowest, prefer-stable ] + stability: [ prefer-stable ] + include: + - laravel: 10.* + testbench: 8.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: none + + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction + + - name: Execute tests + run: vendor/bin/pest + env: + ZAMMAD_URL: ${{ secrets.ZAMMAD_URL }} + ZAMMAD_TOKEN: ${{ secrets.ZAMMAD_TOKEN }} + ZAMMAD_OBJECT_REFERENCE_ERROR_IGNORE: true + + - name: Store test reports + uses: actions/upload-artifact@v2 + with: + name: Store report + retention-days: 1 + path: | + ./reports diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..adac56e --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.idea +.php_cs +.php_cs.cache +.php-cs-fixer.cache +.phpunit.result.cache +.DS_STORE +build +composer.lock +coverage +docs +phpunit.xml +psalm.xml +testbench.yaml +vendor +node_modules +phpstan.neon diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..8d8a790 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,40 @@ +in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => true, + 'trailing_comma_in_multiline' => true, + 'phpdoc_scalar' => true, + 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'class_attributes_separation' => [ + 'elements' => [ + 'method' => 'one', + ], + ], + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ], + 'single_trait_insert_per_statement' => true, + ]) + ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9dc3fbd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +All notable changes to `laravel-flatfox` will be documented in this file. + diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..a9baf1a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) codebar Solutions AG + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b82b81 --- /dev/null +++ b/README.md @@ -0,0 +1,272 @@ + + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/codebar-ag/laravel-zendesk.svg?style=flat-square)](https://packagist.org/packages/codebar-ag/laravel-zendesk) +[![Total Downloads](https://img.shields.io/packagist/dt/codebar-ag/laravel-zendesk.svg?style=flat-square)](https://packagist.org/packages/codebar-ag/laravel-zendesk) +[![run-tests](https://github.com/codebar-ag/laravel-zendesk/actions/workflows/run-tests.yml/badge.svg)](https://github.com/codebar-ag/laravel-zendesk/actions/workflows/run-tests.yml) +[![PHPStan](https://github.com/codebar-ag/laravel-zendesk/actions/workflows/phpstan.yml/badge.svg)](https://github.com/codebar-ag/laravel-zendesk/actions/workflows/phpstan.yml) + +This package was developed to give you a quick start to creating tickets via the Zendesk API. + +## πŸ’‘ What is Zendesk? + +Zendesk is a cloud-based help desk management solution offering customizable tools to build customer service portals, +knowledge base and online communities. + +## πŸ›  Requirements + +| Package | PHP | Laravel | Zendesk | +|-----------|-------|----------------|:---------:| +| >v1.0 | >8.2 | > Laravel 10.0 | βœ… | + +## Authentication + +The currently supported authentication methods are: + +| Method | Supported | +|--------------------|:-----------:| +| Basic Auth | βœ… | +| API token | βœ… | +| OAuth access token | ❌ | + +## βš™οΈ Installation + +You can install the package via composer: + +```bash +composer require codebar-ag/laravel-zendesk +``` + +Optionally, you can publish the config file with: + +```bash +php artisan vendor:publish --provider="CodebarAg\Zendesk\ZendeskServiceProvider" --tag="config" +``` + +You can add the following env variables to your `.env` file: + +```dotenv +ZENDESK_SUBDOMAIN=your-subdomain #required +ZENDESK_AUTHENTICATION_METHOD=token #default ['basic', 'token'] +ZENDESK_EMAIL_ADDRESS=test@example.com #required +ZENDESK_API_TOKEN=your-api-token #required only for token authentication +ZENDESK_API_PASSWORD=your-password #required only for basic authentication +``` + +`Note: We handle base64 encoding for you so you don't have to encode your credentials.` + +You can retrieve your API token from +your [Zendesk Dashboard](https://developer.zendesk.com/api-reference/introduction/security-and-auth/) + +## Usage + +To use the package, you need to create a ZendeskConnector instance. + +```php +use CodebarAg\Zendesk\ZendeskConnector; +... + +$connector = new ZendeskConnector(); +```` + +### Requests + +The following requests are currently supported: + +| Request | Supported | +|-------------------|:-----------:| +| List Tickets | βœ… | +| Count Tickets | βœ… | +| Show Ticket | βœ… | +| Create Ticket | βœ… | +| Create Attachment | βœ… | + +### Responses + +The following responses are currently supported for retrieving the response body: + +| Response Methods | Description | Supported | +|-------------------|------------------------------------------------------------------------------------------------------------------------------------|:-----------:| +| body | Returns the HTTP body as a string | βœ… | +| json | Retrieves a JSON response body and json_decodes it into an array. | βœ… | +| object | Retrieves a JSON response body and json_decodes it into an object. | βœ… | +| collect | Retrieves a JSON response body and json_decodes it into a Laravel collection. **Requires illuminate/collections to be installed.** | βœ… | +| dto | Converts the response into a data-transfer object. You must define your DTO first | βœ… | + +See https://docs.saloon.dev/the-basics/responses for more information. + +### Enums + +We provide enums for the following values: + +| Enum | Values | +|-------------------|:---------------------------------------------------------------------:| +| TicketPriority | 'urgent', 'high', 'normal', 'low' | +| TicketType | 'incident', 'problem', 'question', 'task' | +| MalwareScanResult | 'malware_found', 'malware_not_found', 'failed_to_scan', 'not_scanned' | + +`Note: When using the dto method on a response, the enum values will be converted to their respective enum class.` + +### DTOs + +We provide DTOs for the following: + +| DTO | +|-----------------| +| AttachmentDTO | +| ThumbnailDTO | +| UploadDTO | +| CommentDTO | +| AllTicketsDTO | +| CountTicketsDTO | +| SingleTicketDTO | + +`Note: This is the prefered method of interfacing with Requests and Responses however you can still use the json, object and collect methods. and pass arrays to the requests.` + +### Examples + +#### Create a ticket + +```php +use CodebarAg\Zendesk\Requests\CreateSingleTicketRequest; +use CodebarAg\Zendesk\DTOs\SingleTicketDTO; +use CodebarAg\Zendesk\DTOs\CommentDTO; +use CodebarAg\Zendesk\Enums\TicketPriority; +... + +$ticketResponse = $connector->send( + new CreateSingleTicketRequest( + SingleTicketDTO::fromArray([ + 'comment' => CommentDTO::fromArray([ + 'body' => 'The smoke is very colorful.', + ]), + 'priority' => TicketPriority::URGENT, + "subject" => "My printer is on fire!", + "custom_fields" => [ + [ + "id" => 12345678910111, + "value" => "Your custom field value" + ], + [ + "id" => 12345678910112, + "value" => "Your custom field value 2" + ], + ], + ]) + ) +); + +$ticket = $ticketResponse->dto(); +```` + +#### List all tickets + +```php +use CodebarAg\Zendesk\Requests\AllTicketsRequest; +... + +$listTicketResponse = $connector->send(new AllTicketsRequest()); +$listTicketResponse->dto(); +```` + +#### Count all tickets + +```php +use CodebarAg\Zendesk\Requests\CountTicketsRequest; +... + +$countTicketResponse = $connector->send(new CountTicketsRequest()); +$countTicketResponse->dto(); +```` + +#### Show a ticket + +```php +use CodebarAg\Zendesk\Requests\ShowTicketRequest; +... + +$ticketID = 1; + +$showTicketResponse = $connector->send(new ShowTicketRequest($ticketID)); +$showTicketResponse->dto(); +```` + +#### Upload an attachment + +```php +use CodebarAg\Zendesk\Requests\CreateAttachmentRequest; +use CodebarAg\Zendesk\Requests\CreateSingleTicketRequest; +use Illuminate\Support\Facades\Storage; + +$uploadResponse = $connector->send( + new CreateAttachmentRequest( + fileName: 'someimage.png', + mimeType: Storage::disk('local')->mimeType('public/someimage.png'), + stream: Storage::disk('local')->readStream('public/someimage.png') + ) +); + +$token = $uploadResponse->dto()->token; + +$ticketResponse = $connector->send( + new CreateSingleTicketRequest( + SingleTicketDTO::fromArray([ + 'comment' => CommentDTO::fromArray([ + ... + 'uploads' => [ + $token, + ], + ]), + ]) + ) +); + +$ticket = $ticketResponse->dto(); +``` + +## 🚧 Testing + +Copy your own phpunit.xml-file. + +```bash +cp phpunit.xml.dist phpunit.xml +``` + +Run the tests: + +```bash +./vendor/bin/pest +``` + +## πŸ“ Changelog + +Please see [CHANGELOG](CHANGELOG.md) for recent changes. + +## ✏️ Contributing + +Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. + +```bash +composer test +``` + +### Code Style + +```bash +./vendor/bin/pint +``` + +## πŸ§‘β€πŸ’» Security Vulnerabilities + +Please review [our security policy](.github/SECURITY.md) on reporting security vulnerabilities. + +## πŸ™ Credits +- [Rhys Lees](https://github.com/RhysLees) +- [Sebastian Fix](https://github.com/StanBarrows) +- [All Contributors](../../contributors) +- [Skeleton Repository from Spatie](https://github.com/spatie/package-skeleton-laravel) +- [Laravel Package Training from Spatie](https://spatie.be/videos/laravel-package-training) +- [Laravel Saloon by Sam CarrΓ©](https://github.com/Sammyjo20/Saloon) + +## 🎭 License + +The MIT License (MIT). Please have a look at [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6a50ef4 --- /dev/null +++ b/composer.json @@ -0,0 +1,79 @@ +{ + "name": "codebar-ag/laravel-zendesk", + "description": "Zendesk integration with Laravel", + "keywords": [ + "zendesk", + "laravel", + "codebar-ag", + "laravel-zendesk" + ], + "homepage": "https://github.com/codebar-ag/laravel-zendesk", + "license": "MIT", + "authors": [ + { + "name": "Sebastian Fix", + "email": "sebastian.fix@codebar.ch", + "homepage": "https://www.codebar.ch", + "role": "Developer" + } + ], + "require": { + "php": "^8.2", + "guzzlehttp/guzzle": "^7.2", + "illuminate/contracts": "^10.0", + "saloonphp/cache-plugin": "^2.1", + "sammyjo20/saloon": "^2.0", + "sammyjo20/saloon-laravel": "^2.0", + "spatie/laravel-data": "^3.6", + "spatie/laravel-package-tools": "^1.9.2" + }, + "require-dev": { + "laravel/pint": "^1.5", + "nunomaduro/collision": "^7.0", + "nunomaduro/larastan": "^2.4.0", + "orchestra/testbench": "^8.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "spatie/laravel-ray": "^1.9" + }, + "autoload": { + "psr-4": { + "CodebarAg\\Zendesk\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "CodebarAg\\Zendesk\\Tests\\": "tests" + } + }, + "scripts": { + "analyse": "vendor/bin/phpstan analyse", + "test": "vendor/bin/pest", + "test-coverage": "vendor/bin/pest --coverage", + "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": false, + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "extra": { + "laravel": { + "providers": [ + "CodebarAg\\Zendesk\\ZendeskServiceProvider" + ], + "aliases": { + "Flatfox": "CodebarAg\\Zendesk\\Facades\\Zendesk" + } + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/zendesk.php b/config/zendesk.php new file mode 100644 index 0000000..83fb389 --- /dev/null +++ b/config/zendesk.php @@ -0,0 +1,12 @@ + env('ZENDESK_SUBDOMAIN'), // 'yoursubdomain' + + 'auth' => [ + 'method' => env('ZENDESK_AUTHENTICATION_METHOD', 'token'), // 'basic' or 'token' + 'email_address' => env('ZENDESK_EMAIL_ADDRESS'), // Used for both authentication methods + 'password' => env('ZENDESK_PASSWORD'), // Only used if 'basic' is selected as authentication method + 'api_token' => env('ZENDESK_API_TOKEN'), // Only used if 'apitoken' is selected as authentication method + ], +]; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..e69de29 diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..e005ac7 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,13 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 4 + paths: + - src + - config + tmpDir: build/phpstan + checkOctaneCompatibility: true + checkModelProperties: true + checkMissingIterableValueType: false + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..feb4821 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,44 @@ + + + + + tests + + + + + ./src + + + + + + + + + + + + + + + + diff --git a/src/Dto/Tickets/AllTicketsDTO.php b/src/Dto/Tickets/AllTicketsDTO.php new file mode 100644 index 0000000..3c6eb85 --- /dev/null +++ b/src/Dto/Tickets/AllTicketsDTO.php @@ -0,0 +1,31 @@ +json(); + + return new self( + tickets: collect($data['tickets'])->map(function (array $ticket) { + return SingleTicketDTO::fromArray($ticket); + })->toArray(), + count: $data['count'], + next_page_url: $data['next_page'], + previous_page_url: $data['previous_page'], + ); + } +} diff --git a/src/Dto/Tickets/Attachments/AttachmentDTO.php b/src/Dto/Tickets/Attachments/AttachmentDTO.php new file mode 100644 index 0000000..23324ed --- /dev/null +++ b/src/Dto/Tickets/Attachments/AttachmentDTO.php @@ -0,0 +1,66 @@ + $thumbnail) { + $thumbnails[$key] = self::getThumbnail($thumbnail); + } + } + + return new self( + content_type: $data['content_type'] ?? null, + content_url: $data['content_url'] ?? null, + deleted: $data['deleted'] ?? null, + file_name: $data['file_name'] ?? null, + height: $data['height'] ?? null, + id: $data['id'] ?? null, + inline: $data['inline'] ?? null, + malware_access_override: $data['malware_access_override'] ?? null, + malware_scan_result: MalwareScanResult::tryFrom($data['malware_scan_result'] ?? null), + mapped_content_url: $data['mapped_content_url'] ?? null, + size: $data['size'] ?? null, + thumbnails: $thumbnails ?? null, + url: $data['url'] ?? null, + width: $data['width'] ?? null, + ); + } + + private static function getThumbnail(null|array|ThumbnailDTO $data): ThumbnailDTO + { + $attachment = $data ?? null; + + if (! $attachment instanceof ThumbnailDTO) { + $attachment = ThumbnailDTO::fromArray($attachment); + } + + return $attachment; + } +} diff --git a/src/Dto/Tickets/Attachments/ThumbnailDTO.php b/src/Dto/Tickets/Attachments/ThumbnailDTO.php new file mode 100644 index 0000000..d4930fd --- /dev/null +++ b/src/Dto/Tickets/Attachments/ThumbnailDTO.php @@ -0,0 +1,45 @@ +json()['upload']; + + return self::fromArray($data); + } + + public static function fromArray(array $data): self + { + $attachments = $data['attachments'] ?? null; + + if ($attachments) { + foreach ($attachments as $key => $attachment) { + $attachments[$key] = self::getAttachment($attachment); + } + } + + return new self( + token: $data['token'] ?? null, + expires_at: Carbon::parse($data['expires_at'] ?? null), + attachments: $attachments, + attachment: self::getAttachment($data['attachment'] ?? null), + ); + } + + private static function getAttachment(null|array|AttachmentDTO $data): AttachmentDTO + { + $attachment = $data ?? null; + + if (! $attachment instanceof AttachmentDTO) { + $attachment = AttachmentDTO::fromArray($attachment); + } + + return $attachment; + } +} diff --git a/src/Dto/Tickets/Comments/CommentDTO.php b/src/Dto/Tickets/Comments/CommentDTO.php new file mode 100644 index 0000000..789c7bf --- /dev/null +++ b/src/Dto/Tickets/Comments/CommentDTO.php @@ -0,0 +1,45 @@ +json()['count']; + + return new self( + value: $data['value'], + refreshed_at: Carbon::parse($data['refreshed_at']), + ); + } +} diff --git a/src/Dto/Tickets/SingleTicketDTO.php b/src/Dto/Tickets/SingleTicketDTO.php new file mode 100644 index 0000000..c559373 --- /dev/null +++ b/src/Dto/Tickets/SingleTicketDTO.php @@ -0,0 +1,150 @@ +json()['ticket']; + + return self::fromArray($data); + } + + public static function fromArray(array $data): self + { + $comment = array_key_exists('comment', $data) ? $data['comment'] : null; + + if ($comment && ! $comment instanceof CommentDTO) { + $comment = CommentDTO::fromArray($comment); + } + + $priority = array_key_exists('priority', $data) ? $data['priority'] : null; + + if ($priority && ! $priority instanceof TicketPriority) { + $priority = TicketPriority::tryFrom($priority); + } + + $type = array_key_exists('type', $data) ? $data['type'] : null; + + if ($type && ! $type instanceof TicketType) { + $type = TicketType::tryFrom($type); + } + + return new self( + allow_attachments: $data['allow_attachments'] ?? null, + allow_channelback: $data['allow_channelback'] ?? null, + assignee_email: $data['assignee_email'] ?? null, + assignee_id: $data['assignee_id'] ?? null, + attribute_value_ids: $data['attribute_value_ids'] ?? null, + brand_id: $data['brand_id'] ?? null, + collaborator_ids: $data['collaborator_ids'] ?? null, + collaborators: $data['collaborators'] ?? null, + comment: $comment, + created_at: Carbon::parse($data['created_at'] ?? null), + custom_fields: $data['custom_fields'] ?? null, + description: $data['description'] ?? null, + due_at: Carbon::parse($data['due_at'] ?? null), + email_cc_ids: $data['email_cc_ids'] ?? null, + email_ccs: $data['email_ccs'] ?? null, + external_id: $data['external_id'] ?? null, + follower_ids: $data['follower_ids'] ?? null, + followers: $data['followers'] ?? null, + followup_ids: $data['followup_ids'] ?? null, + forum_topic_id: $data['forum_topic_id'] ?? null, + from_messaging_channel: $data['from_messaging_channel'] ?? null, + group_id: $data['group_id'] ?? null, + has_incidents: $data['has_incidents'] ?? null, + id: $data['id'] ?? null, + is_public: $data['is_public'] ?? null, + macro_id: $data['macro_id'] ?? null, + macro_ids: $data['macro_ids'] ?? null, + metadata: $data['metadata'] ?? null, + organization_id: $data['organization_id'] ?? null, + priority: $priority, + problem_id: $data['problem_id'] ?? null, + raw_subject: $data['raw_subject'] ?? null, + recipient: $data['recipient'] ?? null, + requester: $data['requester'] ?? null, + requester_id: $data['requester_id'] ?? null, + self_update: $data['self_update'] ?? null, + satisfaction_rating: $data['satisfaction_rating'] ?? null, + sharing_agreement_ids: $data['sharing_agreement_ids'] ?? null, + status: $data['status'] ?? null, + subject: $data['subject'] ?? null, + submitter_id: $data['submitter_id'] ?? null, + tags: $data['tags'] ?? null, + ticket_form_id: $data['ticket_form_id'] ?? null, + type: $type, + updated_at: Carbon::parse($data['updated_at'] ?? null), + updated_stamp: $data['updated_stamp'] ?? null, + url: $data['url'] ?? null, + via: $data['via'] ?? null, + via_followup_source_id: $data['via_followup_source_id'] ?? null, + via_id: $data['via_id'] ?? null, + voice_comment: $data['voice_comment'] ?? null, + ); + } +} diff --git a/src/Enums/MalwareScanResult.php b/src/Enums/MalwareScanResult.php new file mode 100644 index 0000000..4bfa7ae --- /dev/null +++ b/src/Enums/MalwareScanResult.php @@ -0,0 +1,11 @@ +fileName; + } + + public function __construct( + protected string $fileName, + protected string $mimeType, + protected mixed $stream, + ) { + } + + protected function defaultHeaders(): array + { + return [ + 'Content-Type' => $this->mimeType, + 'Accept' => 'application/json', + ]; + } + + protected function defaultBody(): mixed + { + return $this->stream; + } + + public function createDtoFromResponse(Response $response): mixed + { + return UploadDTO::fromResponse($response); + } +} diff --git a/src/Requests/CreateSingleTicketRequest.php b/src/Requests/CreateSingleTicketRequest.php new file mode 100644 index 0000000..23621ef --- /dev/null +++ b/src/Requests/CreateSingleTicketRequest.php @@ -0,0 +1,45 @@ +createTicket; + + if (! $body instanceof SingleTicketDTO) { + $body = SingleTicketDTO::fromArray($body); + } + + return [ + 'ticket' => $body->toArray(), + ]; + } + + public function createDtoFromResponse(Response $response): mixed + { + return SingleTicketDTO::fromResponse($response); + } +} diff --git a/src/Requests/SingleTicketRequest.php b/src/Requests/SingleTicketRequest.php new file mode 100644 index 0000000..bacd226 --- /dev/null +++ b/src/Requests/SingleTicketRequest.php @@ -0,0 +1,30 @@ +ticketId = $ticketId; + } + + public function resolveEndpoint(): string + { + return '/tickets/'.$this->ticketId.'.json'; + } + + public function createDtoFromResponse(Response $response): mixed + { + return SingleTicketDTO::fromResponse($response); + } +} diff --git a/src/Zendesk.php b/src/Zendesk.php new file mode 100644 index 0000000..f87aab4 --- /dev/null +++ b/src/Zendesk.php @@ -0,0 +1,7 @@ + 'application/json', + 'Accept' => 'application/json', + ]; + } + + protected function defaultAuth(): ?Authenticator + { + if (! config('zendesk.auth.method')) { + throw new \Exception('No authentication method provided.', 500); + } + + if (! config('zendesk.auth.email_address')) { + throw new \Exception('No email address provided.', 500); + } + + if (config('zendesk.auth.method') === 'basic' && ! config('zendesk.auth.password')) { + throw new \Exception('No password provided for basic authentication.', 500); + } + + if (config('zendesk.auth.method') === 'basic' && ! config('zendesk.auth.password')) { + throw new \Exception('No password provided for basic authentication.', 500); + } + + if (config('zendesk.auth.method') === 'token' && ! config('zendesk.auth.api_token')) { + throw new \Exception('No API token provided for token authentication.', 500); + } + + $authenticationString = match (config('zendesk.auth.method')) { + 'basic' => $authenticationString = config('zendesk.auth.email_address').':'.config('zendesk.auth.password'), + 'token' => $authenticationString = config('zendesk.auth.email_address').'/token:'.config('zendesk.auth.api_token'), + default => throw new \Exception('Invalid authentication method provided.', 500), + }; + + return new TokenAuthenticator(base64_encode($authenticationString), 'Basic'); + } +} diff --git a/src/ZendeskServiceProvider.php b/src/ZendeskServiceProvider.php new file mode 100644 index 0000000..96ff450 --- /dev/null +++ b/src/ZendeskServiceProvider.php @@ -0,0 +1,16 @@ +name('laravel-zendesk') + ->hasConfigFile(); + } +} diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..cc8b9d4 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,10 @@ +in(__DIR__); + +uses()->beforeEach(function () { + Event::fake(); +})->in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..6b100a4 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,36 @@ + 'CodebarAg\\Zendesk\\Database\\Factories\\'.class_basename($modelName).'Factory' + ); + } + + protected function getPackageProviders($app): array + { + return [ + ZendeskServiceProvider::class, + ]; + } + + public function getEnvironmentSetUp($app): void + { + $app['config']->set('database.default', 'sqlite'); + $app['config']->set('database.connections.sqlite', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } +} diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..f56f14c --- /dev/null +++ b/todo.md @@ -0,0 +1,144 @@ +## POST /api/v2/tickets + +Takes a ticket object that specifies the ticket properties. The only required property is comment. See Ticket Comments. + +### Body + +``` +//Markdown formatting is supported in body but not in html_body. Example: + +{ +"ticket": { +"comment": { +"body": "The smoke is very colorful." +}, +"priority": "urgent", +"subject": "My printer is on fire!" +} +} +``` + +### Response + +``` +// Status 201 Created + +{ + "ticket": { + "assignee_id": 235323, + "collaborator_ids": [ + 35334, + 234 + ], + "created_at": "2009-07-20T22:55:29Z", + "custom_fields": [ + { + "id": 27642, + "value": "745" + }, + { + "id": 27648, + "value": "yes" + } + ], + "custom_status_id": 123, + "description": "The fire is very colorful.", + "due_at": null, + "external_id": "ahg35h3jh", + "follower_ids": [ + 35334, + 234 + ], + "from_messaging_channel": false, + "group_id": 98738, + "has_incidents": false, + "id": 35436, + "organization_id": 509974, + "priority": "high", + "problem_id": 9873764, + "raw_subject": "{{dc.printer_on_fire}}", + "recipient": "support@company.com", + "requester_id": 20978392, + "satisfaction_rating": { + "comment": "Great support!", + "id": 1234, + "score": "good" + }, + "sharing_agreement_ids": [ + 84432 + ], + "status": "open", + "subject": "Help, my printer is on fire!", + "submitter_id": 76872, + "tags": [ + "enterprise", + "other_tag" + ], + "type": "incident", + "updated_at": "2011-05-05T10:38:52Z", + "url": "https://company.zendesk.com/api/v2/tickets/35436.json", + "via": { + "channel": "web" + } + } +} +``` + +## POST /api/v2/uploads + +### DTO + +``` +{ + "content_type": "image/png", + "content_url": "https://company.zendesk.com/attachments/my_funny_profile_pic.png", + "file_name": "my_funny_profile_pic.png", + "id": 928374, + "size": 166144, + "thumbnails": [ + { + "content_type": "image/png", + "content_url": "https://company.zendesk.com/attachments/my_funny_profile_pic_thumb.png", + "file_name": "my_funny_profile_pic_thumb.png", + "id": 928375, + "size": 58298 + } + ] +} +``` + +### Response + +``` +{ + "upload": { + "token": "4bLLKSOU63CPqaIeOMXYyXzUh", + "expires_at": "2021-05-08T22:50:18Z", + "attachment": { + "url": "https://example.zendesk.com/api/v2/attachments/1503194928902.json", + "id":1503194928902, + "file_name": "order_issue.png", + "content_url": "https://example.zendesk.com/attachments/token/vp7DnuiSvehLZtK2yrPjqJ1l6/?name=order_issue.png", + "content_type": "image/png" + }, + "attachments": [ + { + "url": "https://example.zendesk.com/api/v2/attachments/1503194928902.json", + "id":1503194928902, + "file_name": "order_issue.png", + "content_url": "https://example.zendesk.com/attachments/token/vp7DnuiSvehLZtK2yrPjqJ1l6/?name=order_issue.png", + "content_type": "image/png" + } + ] + } +} +``` + +## PUT /api/v2/tickets/45135 + +``` +curl https://example.zendesk.com/api/v2/tickets/45135 \ +-d '{"ticket": {"comment": {"body": "Press play", "uploads": ["4bLLKSOU63CPqaIeOMXYyXzUh"]}}}' \ +-H "Content-Type: application/json" \ +-v -u {email_address}:{password} -X PUT +```