Repository: GitLabPHP/Client (php)

Issue:
Add support for additional filters in the Deployment API
In https://docs.gitlab.com/ee/api/deployments.html there are additional filters available and I'd like to use at least a few of them...  

I'll submit a MR shortly with the updates I think are needed...  

Pull request:
[11.13] Addition of Status & Environment to list of possible values
Added Status an Environment as parameters that can be sent to deployment method.  This is to fix #768 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfce5c8..171f989 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,13 +5,20 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [11.12.0] - 2023-10-XX
+## [11.13.0] - UPCOMING
+
+* Add support for `symfony/options-resolver:^7.0`
+
+## [11.12.0] - 2023-10-08
 
 * Add PHP 8.3 support
-* Add support for `environment_scope` in `Project::removeVariable`
+* Add `Projects::updateProtectedBranch` and `Projects::updateApprovalsConfiguration`
+* Add support for `environment_scope` in `Projects::removeVariable`
+* Add support for `filter` in `Projects::variable`
 * Add support for `author` in `Repositories::commits`
+* Add support for additional parameters in `Projects::labels` and `Groups::labels`
 
-## [11.11.1] - 2023-10-XX
+## [11.11.1] - 2023-10-08
 
 * Fixed double encoding of job name in artifacts download
 
diff --git a/README.md b/README.md
index 8e3d18e..a7c598f 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ Check out the [change log](CHANGELOG.md), [releases](https://github.com/GitLabPH
 
 ## Installation
 
-This version supports [PHP](https://php.net) 7.4-8.2. To get started, simply require the project using [Composer](https://getcomposer.org). You will also need to install packages that "provide" [`psr/http-client-implementation`](https://packagist.org/providers/psr/http-client-implementation) and [`psr/http-factory-implementation`](https://packagist.org/providers/psr/http-factory-implementation).
+This version supports [PHP](https://php.net) 7.4-8.3. To get started, simply require the project using [Composer](https://getcomposer.org). You will also need to install packages that "provide" [`psr/http-client-implementation`](https://packagist.org/providers/psr/http-client-implementation) and [`psr/http-factory-implementation`](https://packagist.org/providers/psr/http-factory-implementation).
 
 ### Standard Installation
 
@@ -37,7 +37,7 @@ $ composer require "m4tthumphrey/php-gitlab-api:^11.13" \
 #### Laravel:
 
 ```bash
-$ composer require "graham-campbell/gitlab:^7.5"
+$ composer require "graham-campbell/gitlab:^7.4"
 ```
 
 #### Symfony:
diff --git a/composer.json b/composer.json
index a2add51..96efc59 100644
--- a/composer.json
+++ b/composer.json
@@ -29,16 +29,16 @@
         "php": "^7.4.15 || ^8.0.2",
         "ext-json": "*",
         "ext-xml": "*",
-        "php-http/cache-plugin": "^1.8",
-        "php-http/client-common": "^2.7",
-        "php-http/discovery": "^1.19",
+        "php-http/cache-plugin": "^1.8.1",
+        "php-http/client-common": "^2.7.1",
+        "php-http/discovery": "^1.19.2",
         "php-http/httplug": "^2.4",
         "php-http/multipart-stream-builder": "^1.3",
         "psr/cache": "^1.0 || ^2.0 || ^3.0",
         "psr/http-client-implementation": "^1.0",
         "psr/http-factory-implementation": "^1.0",
         "psr/http-message": "^1.1 || ^2.0",
-        "symfony/options-resolver": "^4.4 || ^5.0 || ^6.0",
+        "symfony/options-resolver": "^4.4 || ^5.0 || ^6.0 || ^7.0",
         "symfony/polyfill-php80": "^1.26"
     },
     "require-dev": {
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 892f32f..4a74598 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -7,7 +7,6 @@ includes:
 rules:
     - Ergebnis\PHPStan\Rules\Closures\NoNullableReturnTypeDeclarationRule
     - Ergebnis\PHPStan\Rules\Expressions\NoCompactRule
-    - Ergebnis\PHPStan\Rules\Expressions\NoEmptyRule
     - Ergebnis\PHPStan\Rules\Expressions\NoEvalRule
     - Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule
     - Ergebnis\PHPStan\Rules\Methods\PrivateInFinalClassRule
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 4ac266f..b689bdf 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,13 +1,13 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutOutputDuringTests="true" bootstrap="vendor/autoload.php" colors="true" failOnRisky="true" failOnWarning="true" processIsolation="false" stopOnError="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd">
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutOutputDuringTests="true" bootstrap="vendor/autoload.php" colors="true" failOnRisky="true" failOnWarning="true" processIsolation="false" stopOnError="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd">
   <testsuites>
     <testsuite name="Test Suite">
       <directory suffix="Test.php">./tests</directory>
     </testsuite>
   </testsuites>
-  <coverage>
+  <source>
     <include>
       <directory suffix=".php">./src</directory>
     </include>
-  </coverage>
+  </source>
 </phpunit>
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index ac9935d..ec73699 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<files psalm-version="5.7.6@ae4ec68e00e4880e3f00b1edd2da891236d749ab">
+<files psalm-version="5.16.0@2897ba636551a8cb61601cc26f6ccfbba6c36591">
   <file src="src/Api/AbstractApi.php">
     <InvalidArgument>
       <code><![CDATA[function () use ($filename, $mode, &$ex): void {
diff --git a/src/Api/AbstractApi.php b/src/Api/AbstractApi.php
index cb53f85..c702613 100644
--- a/src/Api/AbstractApi.php
+++ b/src/Api/AbstractApi.php
@@ -163,6 +163,33 @@ abstract class AbstractApi
         return ResponseMediator::getContent($response);
     }
 
+    /**
+     * @param string               $uri
+     * @param array<string,mixed>  $params
+     * @param array<string,string> $headers
+     * @param array<string,string> $files
+     *
+     * @return mixed
+     */
+    protected function patch(string $uri, array $params = [], array $headers = [], array $files = [])
+    {
+        if (0 < \count($files)) {
+            $builder = $this->createMultipartStreamBuilder($params, $files);
+            $body = self::prepareMultipartBody($builder);
+            $headers = self::addMultipartContentType($headers, $builder);
+        } else {
+            $body = self::prepareJsonBody($params);
+
+            if (null !== $body) {
+                $headers = self::addJsonContentType($headers);
+            }
+        }
+
+        $response = $this->client->getHttpClient()->patch(self::prepareUri($uri), $headers, $body ?? '');
+
+        return ResponseMediator::getContent($response);
+    }
+
     /**
      * @param string               $uri
      * @param string               $file
diff --git a/src/Api/Deployments.php b/src/Api/Deployments.php
index f0fcbcc..9af4169 100644
--- a/src/Api/Deployments.php
+++ b/src/Api/Deployments.php
@@ -23,6 +23,10 @@ class Deployments extends AbstractApi
      *     @var string $order_by                    Return deployments ordered by id, iid, created_at, updated_at,
      *                                              or ref fields (default is id)
      *     @var string $sort                        Return deployments sorted in asc or desc order (default is desc)
+     *     @var string $status                      Return deployments filtered by status of deployment allowed
+     *                                              values of status are 'created', 'running', 'success', 'failed',
+     *                                              'canceled', 'blocked'
+     *     @var string $environment                 Return deployments filtered to a particular environment
      * }
      *
      * @return mixed
@@ -36,6 +40,11 @@ class Deployments extends AbstractApi
         $resolver->setDefined('sort')
             ->setAllowedTypes('sort', 'string')
             ->setAllowedValues('sort', ['desc', 'asc']);
+        $resolver->setDefined('status')
+            ->setAllowedTypes('status', 'string')
+            ->setAllowedValues('status', ['created', 'running', 'success', 'failed', 'canceled', 'blocked']);
+        $resolver->setDefined('environment')
+            ->setAllowedTypes('environment', 'string');
 
         return $this->get($this->getProjectPath($project_id, 'deployments'), $resolver->resolve($parameters));
     }
diff --git a/src/Api/Groups.php b/src/Api/Groups.php
index 8b46932..3c0ee3e 100644
--- a/src/Api/Groups.php
+++ b/src/Api/Groups.php
@@ -506,7 +506,14 @@ class Groups extends AbstractApi
 
     /**
      * @param int|string $group_id
-     * @param array      $parameters
+     * @param array      $parameters {
+     *
+     *     @var bool     $with_counts               Whether or not to include issue and merge request counts. Defaults to false.
+     *     @var bool     $include_ancestor_groups   Include ancestor groups. Defaults to true.
+     *     @var bool     $include_descendant_groups Include descendant groups. Defaults to false.
+     *     @var bool     $only_group_labels         Toggle to include only group labels or also project labels. Defaults to true.
+     *     @var string   $search                    Keyword to filter labels by.
+     * }
      *
      * @return mixed
      */
@@ -514,6 +521,21 @@ class Groups extends AbstractApi
     {
         $resolver = $this->createOptionsResolver();
 
+        $resolver->setDefined('with_counts')
+            ->setAllowedTypes('with_counts', 'bool');
+
+        $resolver->setDefined('include_ancestor_groups')
+            ->setAllowedTypes('include_ancestor_groups', 'bool');
+
+        $resolver->setDefined('include_descendant_groups')
+            ->setAllowedTypes('include_descendant_groups', 'bool');
+
+        $resolver->setDefined('only_group_labels')
+            ->setAllowedTypes('only_group_labels', 'bool');
+
+        $resolver->setDefined('search')
+            ->setAllowedTypes('search', 'string');
+
         return $this->get('groups/'.self::encodePath($group_id).'/labels', $resolver->resolve($parameters));
     }
 
diff --git a/src/Api/Projects.php b/src/Api/Projects.php
index ebec6d9..a4d1c14 100644
--- a/src/Api/Projects.php
+++ b/src/Api/Projects.php
@@ -976,7 +976,12 @@ class Projects extends AbstractApi
 
     /**
      * @param int|string $project_id
-     * @param array      $parameters
+     * @param array      $parameters {
+     *
+     *     @var bool     $with_counts               Whether or not to include issue and merge request counts. Defaults to false.
+     *     @var bool     $include_ancestor_groups   Include ancestor groups. Defaults to true.
+     *     @var string   $search                    Keyword to filter labels by.
+     * }
      *
      * @return mixed
      */
@@ -984,6 +989,15 @@ class Projects extends AbstractApi
     {
         $resolver = $this->createOptionsResolver();
 
+        $resolver->setDefined('with_counts')
+            ->setAllowedTypes('with_counts', 'bool');
+
+        $resolver->setDefined('include_ancestor_groups')
+            ->setAllowedTypes('include_ancestor_groups', 'bool');
+
+        $resolver->setDefined('search')
+            ->setAllowedTypes('search', 'string');
+
         return $this->get($this->getProjectPath($project_id, 'labels'), $resolver->resolve($parameters));
     }
 
@@ -1214,12 +1228,17 @@ class Projects extends AbstractApi
     /**
      * @param int|string $project_id
      * @param string     $key
+     * @param array      $parameters
      *
      * @return mixed
      */
-    public function variable($project_id, string $key)
+    public function variable($project_id, string $key, array $parameters = [])
     {
-        return $this->get($this->getProjectPath($project_id, 'variables/'.self::encodePath($key)));
+        $resolver = $this->createOptionsResolver();
+        $resolver->setDefined('filter')
+            ->setAllowedTypes('filter', 'array');
+
+        return $this->get($this->getProjectPath($project_id, 'variables/'.self::encodePath($key)), $resolver->resolve($parameters));
     }
 
     /**
@@ -1527,6 +1546,18 @@ class Projects extends AbstractApi
         return $this->delete($this->getProjectPath($project_id, 'protected_branches/'.self::encodePath($branch_name)));
     }
 
+    /**
+     * @param int|string $project_id
+     * @param string     $branch_name
+     * @param array      $parameters
+     *
+     * @return mixed
+     */
+    public function updateProtectedBranch($project_id, string $branch_name, array $parameters = [])
+    {
+        return $this->patch($this->getProjectPath($project_id, 'protected_branches/'.self::encodePath($branch_name)), $parameters);
+    }
+
     /**
      * @param int|string $project_id
      *
@@ -1537,6 +1568,17 @@ class Projects extends AbstractApi
         return $this->get('projects/'.self::encodePath($project_id).'/approvals');
     }
 
+    /**
+     * @param int|string $project_id
+     * @param array      $parameters
+     *
+     * @return mixed
+     */
+    public function updateApprovalsConfiguration($project_id, array $parameters = [])
+    {
+        return $this->post('projects/'.self::encodePath($project_id).'/approvals', $parameters);
+    }
+
     /**
      * @param int|string $project_id
      *
diff --git a/src/HttpClient/Plugin/Authentication.php b/src/HttpClient/Plugin/Authentication.php
index 923c981..2160f5c 100644
--- a/src/HttpClient/Plugin/Authentication.php
+++ b/src/HttpClient/Plugin/Authentication.php
@@ -19,6 +19,7 @@ use Gitlab\Exception\RuntimeException;
 use Http\Client\Common\Plugin;
 use Http\Promise\Promise;
 use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
 
 /**
  * Add authentication to the request.
@@ -54,7 +55,7 @@ final class Authentication implements Plugin
      * @param callable         $next
      * @param callable         $first
      *
-     * @return Promise
+     * @return Promise<ResponseInterface>
      */
     public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
     {
diff --git a/src/HttpClient/Plugin/ExceptionThrower.php b/src/HttpClient/Plugin/ExceptionThrower.php
index ac9d188..207ecaa 100644
--- a/src/HttpClient/Plugin/ExceptionThrower.php
+++ b/src/HttpClient/Plugin/ExceptionThrower.php
@@ -42,7 +42,7 @@ final class ExceptionThrower implements Plugin
      * @param callable         $next
      * @param callable         $first
      *
-     * @return Promise
+     * @return Promise<ResponseInterface>
      */
     public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
     {
diff --git a/vendor-bin/phpstan/composer.json b/vendor-bin/phpstan/composer.json
index 88a5f74..38f61a0 100644
--- a/vendor-bin/phpstan/composer.json
+++ b/vendor-bin/phpstan/composer.json
@@ -1,11 +1,11 @@
 {
     "require": {
         "php": "^8.1",
-        "phpstan/phpstan": "1.10.3",
-        "phpstan/phpstan-deprecation-rules": "1.1.2",
-        "phpstan/phpstan-strict-rules": "1.5.0",
+        "phpstan/phpstan": "1.10.47",
+        "phpstan/phpstan-deprecation-rules": "1.1.4",
+        "phpstan/phpstan-strict-rules": "1.5.2",
         "thecodingmachine/phpstan-strict-rules": "1.0.0",
-        "ergebnis/phpstan-rules": "1.0.0"
+        "ergebnis/phpstan-rules": "2.1.0"
     },
     "config": {
         "preferred-install": "dist"
diff --git a/vendor-bin/phpunit/composer.json b/vendor-bin/phpunit/composer.json
index 59819b4..ce2646e 100644
--- a/vendor-bin/phpunit/composer.json
+++ b/vendor-bin/phpunit/composer.json
@@ -1,7 +1,7 @@
 {
     "require": {
         "php": "^7.4.15 || ^8.0.2",
-        "phpunit/phpunit": "^9.6.3 || ^10.0.12"
+        "phpunit/phpunit": "^9.6.15 || ^10.5.1"
     },
     "config": {
         "preferred-install": "dist"
diff --git a/vendor-bin/psalm/composer.json b/vendor-bin/psalm/composer.json
index 5060e69..5a8af7c 100644
--- a/vendor-bin/psalm/composer.json
+++ b/vendor-bin/psalm/composer.json
@@ -1,7 +1,7 @@
 {
     "require": {
         "php": "^8.1",
-        "psalm/phar": "5.7.6"
+        "psalm/phar": "5.16.0"
     },
     "config": {
         "preferred-install": "dist"


Test patch:
diff --git a/tests/Api/DeploymentsTest.php b/tests/Api/DeploymentsTest.php
index 0f84792..6629a59 100644
--- a/tests/Api/DeploymentsTest.php
+++ b/tests/Api/DeploymentsTest.php
@@ -278,4 +278,59 @@ See merge request !2',
     {
         return Deployments::class;
     }
+
+    /**
+     * @test
+     */
+    public function shouldAllowDeploymentFilterByStatus(): void
+    {
+        $expectedArray = $this->getMultipleDeploymentsData();
+
+        $api = $this->getMultipleDeploymentsRequestMock(
+            'projects/1/deployments',
+            $expectedArray,
+            ['status' => 'success']
+        );
+
+        $this->assertEquals(
+            $expectedArray,
+            $api->all(1, ['status' => 'success'])
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function shouldAllowFilterByEnvironment(): void
+    {
+        $expectedArray = $this->getMultipleDeploymentsData();
+
+        $api = $this->getMultipleDeploymentsRequestMock(
+            'projects/1/deployments',
+            $expectedArray,
+            ['environment' => 'production']
+        );
+
+        $this->assertEquals(
+            $expectedArray,
+            $api->all(1, ['environment' => 'production'])
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function shouldAllowEmptyArrayIfAllExcludedByFilter(): void
+    {
+        $expectedArray = $this->getMultipleDeploymentsData();
+
+        $api = $this->getMultipleDeploymentsRequestMock(
+            'projects/1/deployments',
+            [],
+            ['environment' => 'test']
+        );
+
+        $this->assertEquals([], $api->all(1, ['environment' => 'test'])
+        );
+    }
 }