From 6ed49300a8a60ed55025db39864ea2dec6579b4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:44:06 +0000 Subject: [PATCH 1/4] Initial plan From 696712e1a477d21a3161451df2c44a32af674d7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:00:49 +0000 Subject: [PATCH 2/4] Set up PHP development infrastructure --- .github/workflows/ci.yml | 61 - .github/workflows/tests.yml | 62 + composer.json | 43 +- examples/config.php.dist | 1 + examples/tag_admin.php | 1 + examples/ticket.php | 1 + examples/user.php | 1 + phpstan-baseline.neon | 1021 +++++++++++++++++ phpstan.neon.dist | 9 + phpunit.xml.dist | 28 +- src/Client.php | 49 +- src/Client/Response.php | 20 +- src/Exception/AbstractException.php | 6 +- .../AlreadyFetchedObjectException.php | 6 +- src/HTTPClient.php | 60 +- src/HTTPClientInterface.php | 2 + src/Resource/AbstractResource.php | 156 +-- src/Resource/Group.php | 7 +- src/Resource/Link.php | 36 +- src/Resource/Organization.php | 16 +- src/Resource/Tag.php | 44 +- src/Resource/TextModule.php | 16 +- src/Resource/Ticket.php | 17 +- src/Resource/TicketArticle.php | 38 +- src/Resource/TicketPriority.php | 7 +- src/Resource/TicketState.php | 7 +- src/Resource/User.php | 16 +- src/ResourceType.php | 25 +- test/ZammadAPIClient/Client/ResponseTest.php | 1 + test/ZammadAPIClient/ClientTest.php | 7 + test/ZammadAPIClient/EnvConfigTrait.php | 1 + test/ZammadAPIClient/GetIDTest.php | 1 + .../Resource/AbstractBaseTest.php | 5 +- test/ZammadAPIClient/Resource/GroupTest.php | 3 + test/ZammadAPIClient/Resource/LinkTest.php | 3 + .../Resource/OrganizationTest.php | 3 + test/ZammadAPIClient/Resource/TagTest.php | 3 + .../Resource/TextModuleTest.php | 3 + .../Resource/TicketArticleTest.php | 3 + .../Resource/TicketPriorityTest.php | 3 + .../Resource/TicketStateTest.php | 3 + test/ZammadAPIClient/Resource/TicketTest.php | 3 + test/ZammadAPIClient/Resource/UserTest.php | 3 + test/bootstrap.php | 66 +- 44 files changed, 1488 insertions(+), 379 deletions(-) delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/tests.yml create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 8ac6f90..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: CI -on: - push: - branches: [master] - pull_request: - schedule: - # Run every on Friday to ensure everything works as expected. - - cron: '0 6 * * 5' -jobs: - CI: - runs-on: ubuntu-latest - container: - image: zammad/zammad-ci:latest - services: - postgresql: - image: postgres:17 - env: - POSTGRES_USER: zammad - POSTGRES_PASSWORD: zammad - redis: - image: redis:7 - env: - ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_URL: "http://localhost:3000" - ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_USERNAME: "admin@example.com" - ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_PASSWORD: "test" - strategy: - fail-fast: false - matrix: - php: ['7.4', '7.3', '7.2', '8.0', '8.1', '8.2', '8.3', '8.4'] - name: PHP ${{ matrix.php }} - steps: - - uses: actions/checkout@v6 - - name: Install PHP - uses: shivammathur/setup-php@v2 - env: - fail-fast: true - with: - php-version: ${{ matrix.php }} - - name: Report PHP version - run: php -v - - name: Install dependencies - shell: bash - run: | - composer install - - name: Set up Zammad - shell: bash - run: | - git clone --depth 1 https://github.com/zammad/zammad.git - cd zammad - source /etc/profile.d/rvm.sh # ensure RVM is loaded - bundle config set --local frozen 'true' - bundle config set --local path 'vendor' - bundle install -j $(nproc) - bundle exec ruby .gitlab/configure_environment.rb - source .gitlab/environment.env - RAILS_ENV=test bundle exec rake db:create - RAILS_ENV=test bundle exec rake zammad:ci:test:start zammad:setup:auto_wizard - - name: Run PHP API integration tests - shell: bash - run: | - vendor/bin/phpunit diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..a4f347d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,62 @@ +name: Tests + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +jobs: + unit: + if: github.event_name != 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run PHPCS + run: vendor/bin/phpcs --standard=PSR12 src/ + + - name: Run PHPStan + run: vendor/bin/phpstan analyse src/ --configuration phpstan.neon.dist + + - name: Run PHPUnit unit suite + run: vendor/bin/phpunit --testsuite unit + + testcontainers: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + services: + docker: + image: docker:dind + options: --privileged + ports: + - 2375:2375 + env: + DOCKER_HOST: tcp://127.0.0.1:2375 + DOCKER_TLS_CERTDIR: '' + steps: + - uses: actions/checkout@v6 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Verify Docker daemon + run: docker info + + - name: Run PHPUnit integration suite + run: vendor/bin/phpunit --no-configuration --bootstrap test/bootstrap.php test/ZammadAPIClient/Resource diff --git a/composer.json b/composer.json index 60dcdeb..88c6a41 100644 --- a/composer.json +++ b/composer.json @@ -3,8 +3,14 @@ "type": "library", "description": "Zammad API client for PHP", "homepage": "https://github.com/zammad/zammad-api-client-php", - "keywords": ["zammad", "api"], - "license": ["AGPL-3.0", "MIT"], + "keywords": [ + "zammad", + "api" + ], + "license": [ + "AGPL-3.0", + "MIT" + ], "authors": [ { "name": "Zammad GmbH", @@ -12,13 +18,38 @@ } ], "require": { - "php": ">=7.2", - "guzzlehttp/guzzle": "^7" + "php": ">=8.3", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.1", + "psr/log": "^3.0" }, "require-dev": { - "phpunit/phpunit": ">=5.7 <9" + "guzzlehttp/guzzle": "^7.11", + "league/openapi-psr7-validator": "^0.22", + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^10.5.62", + "squizlabs/php_codesniffer": "^3.11" + }, + "suggest": { + "guzzlehttp/guzzle": "Provides the default HTTP client implementation used by ZammadAPIClient\\HTTPClient." }, "autoload": { - "psr-4": {"ZammadAPIClient\\": "src/"} + "psr-4": { + "ZammadAPIClient\\": "src/" + } + }, + "autoload-dev": { + "classmap": [ + "test/" + ] + }, + "scripts": { + "phpcs": "phpcs --standard=PSR12 src/", + "phpstan": "phpstan analyse src/ --configuration phpstan.neon.dist", + "test": "phpunit --testsuite=unit" + }, + "config": { + "sort-packages": true } } diff --git a/examples/config.php.dist b/examples/config.php.dist index e9564ef..3db3140 100644 --- a/examples/config.php.dist +++ b/examples/config.php.dist @@ -1,4 +1,5 @@ System => API) diff --git a/examples/tag_admin.php b/examples/tag_admin.php index 6150af9..f805630 100644 --- a/examples/tag_admin.php +++ b/examples/tag_admin.php @@ -1,4 +1,5 @@ \|string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Resource/AbstractResource.php + + - + message: '#^Part \$value \(mixed\) of encapsed string cannot be cast to string\.$#' + identifier: encapsedStringPart.nonString + count: 1 + path: src/Resource/AbstractResource.php + + - + message: '#^Property ZammadAPIClient\\Resource\\AbstractResource\:\:\$client has no type specified\.$#' + identifier: missingType.property + count: 1 + path: src/Resource/AbstractResource.php + + - + message: '#^Property ZammadAPIClient\\Resource\\AbstractResource\:\:\$error has no type specified\.$#' + identifier: missingType.property + count: 1 + path: src/Resource/AbstractResource.php + + - + message: '#^Property ZammadAPIClient\\Resource\\AbstractResource\:\:\$remote_data has no type specified\.$#' + identifier: missingType.property + count: 1 + path: src/Resource/AbstractResource.php + + - + message: '#^Property ZammadAPIClient\\Resource\\AbstractResource\:\:\$values has no type specified\.$#' + identifier: missingType.property + count: 1 + path: src/Resource/AbstractResource.php + + - + message: '#^Result of && is always true\.$#' + identifier: booleanAnd.alwaysTrue + count: 2 + path: src/Resource/AbstractResource.php + + - + message: '#^Variable \$objects_per_page in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 4 + path: src/Resource/AbstractResource.php + + - + message: '#^Variable \$page in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 2 + path: src/Resource/AbstractResource.php + + - + message: '#^Call to an undefined method object\:\:delete\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/Link.php + + - + message: '#^Call to an undefined method object\:\:get\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/Link.php + + - + message: '#^Call to an undefined method object\:\:post\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/Link.php + + - + message: '#^Cannot call method getData\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/Link.php + + - + message: '#^Cannot call method getError\(\) on mixed\.$#' + identifier: method.nonObject + count: 3 + path: src/Resource/Link.php + + - + message: '#^Cannot call method hasError\(\) on mixed\.$#' + identifier: method.nonObject + count: 3 + path: src/Resource/Link.php + + - + message: '#^Parameter \#1 \$error of method ZammadAPIClient\\Resource\\AbstractResource\:\:setError\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 3 + path: src/Resource/Link.php + + - + message: '#^Parameter \#1 \$remote_data of method ZammadAPIClient\\Resource\\AbstractResource\:\:setRemoteData\(\) expects array, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Resource/Link.php + + - + message: '#^Call to an undefined method object\:\:post\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/Organization.php + + - + message: '#^Cannot call method getData\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/Organization.php + + - + message: '#^Cannot call method getError\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/Organization.php + + - + message: '#^Cannot call method hasError\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/Organization.php + + - + message: '#^Parameter \#1 \$error of method ZammadAPIClient\\Resource\\AbstractResource\:\:setError\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Resource/Organization.php + + - + message: '#^Parameter \#1 \$remote_data of method ZammadAPIClient\\Resource\\AbstractResource\:\:setRemoteData\(\) expects array, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Resource/Organization.php + + - + message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' + identifier: foreach.nonIterable + count: 1 + path: src/Resource/Tag.php + + - + message: '#^Call to an undefined method object\:\:delete\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/Tag.php + + - + message: '#^Call to an undefined method object\:\:get\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Resource/Tag.php + + - + message: '#^Call to an undefined method object\:\:post\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/Tag.php + + - + message: '#^Call to an undefined method object\:\:resource\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/Tag.php + + - + message: '#^Cannot call method getData\(\) on mixed\.$#' + identifier: method.nonObject + count: 2 + path: src/Resource/Tag.php + + - + message: '#^Cannot call method getError\(\) on mixed\.$#' + identifier: method.nonObject + count: 4 + path: src/Resource/Tag.php + + - + message: '#^Cannot call method hasError\(\) on mixed\.$#' + identifier: method.nonObject + count: 4 + path: src/Resource/Tag.php + + - + message: '#^Cannot call method setRemoteData\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/Tag.php + + - + message: '#^Parameter \#1 \$error of method ZammadAPIClient\\Resource\\AbstractResource\:\:setError\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 4 + path: src/Resource/Tag.php + + - + message: '#^Parameter \#1 \$remote_data of method ZammadAPIClient\\Resource\\AbstractResource\:\:setRemoteData\(\) expects array, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Resource/Tag.php + + - + message: '#^Call to an undefined method object\:\:post\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/TextModule.php + + - + message: '#^Cannot call method getData\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/TextModule.php + + - + message: '#^Cannot call method getError\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/TextModule.php + + - + message: '#^Cannot call method hasError\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/TextModule.php + + - + message: '#^Parameter \#1 \$error of method ZammadAPIClient\\Resource\\AbstractResource\:\:setError\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Resource/TextModule.php + + - + message: '#^Parameter \#1 \$remote_data of method ZammadAPIClient\\Resource\\AbstractResource\:\:setRemoteData\(\) expects array, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Resource/TextModule.php + + - + message: '#^Call to an undefined method object\:\:resource\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/Ticket.php + + - + message: '#^Cannot call method getError\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/Ticket.php + + - + message: '#^Cannot call method getForTicket\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/Ticket.php + + - + message: '#^Method ZammadAPIClient\\Resource\\Ticket\:\:getTicketArticles\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Resource/Ticket.php + + - + message: '#^Parameter \#1 \$error of method ZammadAPIClient\\Resource\\AbstractResource\:\:setError\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Resource/Ticket.php + + - + message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' + identifier: foreach.nonIterable + count: 1 + path: src/Resource/TicketArticle.php + + - + message: '#^Call to an undefined method object\:\:get\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Resource/TicketArticle.php + + - + message: '#^Call to an undefined method object\:\:resource\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/TicketArticle.php + + - + message: '#^Cannot access offset ''id'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Resource/TicketArticle.php + + - + message: '#^Cannot call method getBody\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/TicketArticle.php + + - + message: '#^Cannot call method getData\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/TicketArticle.php + + - + message: '#^Cannot call method getError\(\) on mixed\.$#' + identifier: method.nonObject + count: 2 + path: src/Resource/TicketArticle.php + + - + message: '#^Cannot call method hasError\(\) on mixed\.$#' + identifier: method.nonObject + count: 2 + path: src/Resource/TicketArticle.php + + - + message: '#^Cannot call method setRemoteData\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/TicketArticle.php + + - + message: '#^Method ZammadAPIClient\\Resource\\TicketArticle\:\:getAttachmentContent\(\) should return string but returns false\.$#' + identifier: return.type + count: 3 + path: src/Resource/TicketArticle.php + + - + message: '#^Method ZammadAPIClient\\Resource\\TicketArticle\:\:getAttachmentContent\(\) should return string but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Resource/TicketArticle.php + + - + message: '#^Method ZammadAPIClient\\Resource\\TicketArticle\:\:getForTicket\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Resource/TicketArticle.php + + - + message: '#^Method ZammadAPIClient\\Resource\\TicketArticle\:\:getForTicket\(\) should return array but returns \$this\(ZammadAPIClient\\Resource\\TicketArticle\)\.$#' + identifier: return.type + count: 1 + path: src/Resource/TicketArticle.php + + - + message: '#^Parameter \#1 \$error of method ZammadAPIClient\\Resource\\AbstractResource\:\:setError\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 2 + path: src/Resource/TicketArticle.php + + - + message: '#^Call to an undefined method object\:\:post\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Resource/User.php + + - + message: '#^Cannot call method getData\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/User.php + + - + message: '#^Cannot call method getError\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/User.php + + - + message: '#^Cannot call method hasError\(\) on mixed\.$#' + identifier: method.nonObject + count: 1 + path: src/Resource/User.php + + - + message: '#^Parameter \#1 \$error of method ZammadAPIClient\\Resource\\AbstractResource\:\:setError\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Resource/User.php + + - + message: '#^Parameter \#1 \$remote_data of method ZammadAPIClient\\Resource\\AbstractResource\:\:setRemoteData\(\) expects array, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Resource/User.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..fe173aa --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,9 @@ +includes: + - phpstan-baseline.neon + +parameters: + bootstrapFiles: + - test/bootstrap.php + level: max + paths: + - src diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2cd5e9e..bbb9b29 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,24 @@ - - + cacheResult="false" + colors="true"> - - ./test/ + + test/ZammadAPIClient + test/ZammadAPIClient/Resource + + + test/ZammadAPIClient/Resource + + + integration + testcontainers + +