gitlab-it/gitlab-sdk

GitLab API Client for Laravel

4.1 2024-03-22 13:15 UTC

This package is auto-updated.

Last update: 2024-04-22 05:12:05 UTC


README

[[TOC]]

Overview

The GitLab API Client is an open source Composer package for use in Laravel applications for connecting to GitLab SaaS or self-managed instances for provisioning and deprovisioning of users, groups, projects, and other related functionality.

This is maintained by the open source community and is not maintained by any company. Please use at your own risk and create merge requests for any bugs that you encounter.

Problem Statement

Instead of providing an SDK method for every endpoint in the API documentation, we have taken a simpler approach by providing a universal ApiClient that can perform GET, POST, PUT, and DELETE requests to any endpoint that you find in the GitLab API documentation.

This builds upon the simplicity of the Laravel HTTP Client that is powered by the Guzzle HTTP client to provide "last lines of code parsing" for GitLab API responses to improve the developer experience.

The value of this API Client is that it handles the API request logging, response pagination, rate limit backoff, and 4xx/5xx exception handling for you.

For a comprehensive SDK with pre-built Laravel Actions for console commands, service class methods, dispatchable jobs, and API endpoints, see the provisionesta/gitlab-laravel-actions package.

Example Usage

use Provisionesta\Gitlab\ApiClient;

// Get a list of records (positional arguments)
// https://docs.gitlab.com/ee/api/projects.html#list-all-projects
$projects = ApiClient::get('/projects');

// Get list of records (named arguments)
$projects = ApiClient::get(
    uri: '/projects'
);

// Search for records
// https://docs.gitlab.com/ee/api/projects.html#list-all-projects
$projects = ApiClient::get(
    uri: '/projects',
    data: [
        'search' => 'my-project-name',
        'membership' => true
    ]
);

// Get a specific record (positional arguments)
// https://docs.gitlab.com/ee/api/projects.html#get-single-project
$project = ApiClient::get('/projects/123456789');

// Get a specific record with URL encoded path
$project = ApiClient::get('/projects/' . ApiClient::urlencode('group-name/child-group-name/project-name'));

// Create a project
// https://docs.gitlab.com/ee/api/projects.html#create-project
$group_id = '12345678';
$record = ApiClient::post(
    uri: '/projects',
    data: [
        'name' => 'My Cool Project',
        'path' => 'my-cool-project',
        'namespace_id' => $group_id
    ]
);

// Update a project
// https://docs.gitlab.com/ee/api/projects.html#edit-project
$project_id = '123456789';
$record = ApiClient::put(
    uri: '/projects/' . $project_id,
    data: [
        'description' => 'This is a cool project that we created for a demo.'
    ]
);

// Delete a project
// https://docs.gitlab.com/ee/api/projects.html#delete-project
$project_id = '123456789';
$record = ApiClient::delete(
    uri: '/projects/' . $project_id
);

Issue Tracking and Bug Reports

We do not maintain a roadmap of feature requests, however we invite you to contribute and we will gladly review your merge requests.

Please create an issue for bug reports.

Contributing

Please see CONTRIBUTING.md to learn more about how to contribute.

Maintainers

NameGitLab HandleEmail
Jeff Martin@jeffersonmartinprovisionesta [at] jeffersonmartin [dot] com

Contributor Credit

Installation

Requirements

RequirementVersion
PHP^8.0, ^8.1, ^8.2, ^8.3
Laravel^8.0, ^9.0, ^10.0, ^11.0

Upgrade Guide

See the changelog for release notes.

Still Using glamstack/gitlab-sdk (v2.x)? See the v3.0 changelog for upgrade instructions.

Still using gitlab-it/gitlab-sdk (v3.x)? See the v4.0 changelog for upgrade instructions.

Add Composer Package

composer require provisionesta/gitlab-api-client:^4.0

If you are contributing to this package, see CONTRIBUTING.md for instructions on configuring a local composer package with symlinks.

Publish the configuration file

This is optional. The configuration file specifies which .env variable names that that the API connection is stored in. You only need to publish the configuration file if you want to rename the GITLAB_API_* .env variable names.

php artisan vendor:publish --tag=gitlab-api-client

Connection Credentials

Environment Variables

Add the following variables to your .env file. You can add these anywhere in the file on a new line, or add to the bottom of the file (your choice).

GITLAB_API_URL="https://gitlab.com"
GITLAB_API_TOKEN=""

If you have your connection secrets stored in your database or secrets manager, you can override the config/gitlab-api-client.php configuration or provide a connection array on each request. See connection arrays to learn more.

URL

If you are using GitLab.com SaaS (you don't host your own GitLab instance), then the URL is https://gitlab.com. If you're just getting started, it is recommended to sign up for a free account on GitLab.com. You can use the API with projects in your personal namespace or for open source or organization groups.

GITLAB_API_URL="https://gitlab.com"
GITLAB_API_TOKEN=""

If you host your own GitLab self-managed instance, then the URL is the FQDN of your instance that you use to sign in (ex. https://gitlab.example.com).

GITLAB_API_URL="https://gitlab.example.com"
GITLAB_API_TOKEN=""

If your GitLab instance is behind a firewall, then you will need to work with your IT or Infrastructure team to allow the Laravel application to connect to the GitLab instance. This configuration varies based on your environment and no support is provided. You can perform testing using CURL commands to https://gitlab.example.com/api/v4/version when establishing initial connectivity.

API Tokens

You need to generate an access token on your GitLab instance and update the GITLAB_API_TOKEN variable in your .env file.

GITLAB_API_TOKEN="glpat-S3cr3tK3yG03sH3r3"

See Security Best Practices before creating an API token.

Connection Arrays

The variables that you define in your .env file are used by default unless you set the connection argument with an array containing the URL and the API token.

Security Warning: Do not commit a hard coded API token into your code base. This should only be used when using dynamic variables that are stored in your database or secrets manager.

$connection = [
    'url' => 'https://gitlab.com',
    'token' => 'glpat-S3cr3tK3yG03sH3r3'
];
use Provisionesta\Gitlab\ApiClient;

class MyClass
{
    private array $connection;

    public function __construct($connection)
    {
        $this->connection = $connection;
    }

    public function getGroup($group_id)
    {
        return ApiClient::get(
            connection: $this->connection,
            uri: 'groups/' . $group_id
        )->data;
    }
}

Security Best Practices

No Shared Tokens

Do not use an API token that you have already created for another purpose. You should generate a new API Token for each use case.

This is helpful during security incidents when a key needs to be revoked on a compromised system and you do not want other systems that use the same user or service account to be affected since they use a different key that wasn't revoked.

API Token Storage

Do not add your API token to any config/*.php files to avoid committing to your repository (secret leak).

All API tokens should be defined in the .env file which is included in .gitignore and not committed to your repository.

For advanced use cases, you can store your variables in CI/CD variables or a secrets vault (ex. Ansible Vault, AWS Parameter Store, GCP Secrets Manager, HashiCorp Vault, etc.).

API Token Permissions

We recommend reading more about Personal Access Tokens, Group Access Tokens, Project Access Tokens, and Security Best Practices before creating an API token for your application.

All API endpoints require the api or read_api scope. If you are not performing any read-write operations, it is recommended to use the read_api scope as a proactive security measure.

If you are using a personal access token, the API token uses the permissions for the user that it belongs to, so it is a best practice to create a service account (bot) user for production application use cases. For safety reasons, most service accounts should be a Regular user. Be very careful if your user account has Administrator access.

GitLab Project Permissions

Each API call maps to a specific permission that is allowed for one or more roles.

You should not configure Owner or Maintainer over-permissive roles for a Group Access Token or Project Access Token unless you have API calls that specifically require this permission level.

https://docs.gitlab.com/ee/user/permissions.html#project-members-permissions

Personal Access Tokens

A Personal Access Token provides access to all of the groups and projects that your user account has access to. This is a widely permissive token and should be used carefully. This is only recommended for use cases that perform admin-level API calls or need access to multiple groups that cannot be performed with a Group Access Token.

For safety reasons, most service accounts should be a Regular user. Be very careful if your user account has Administrator access.

https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html

Group Access Tokens

A Group Access Token only provides access to the specific GitLab group and any child groups and GitLab projects. This is the recommended type of token for most use cases.

https://docs.gitlab.com/ee/user/group/settings/group_access_tokens.html

Project Access Tokens

A Project Access Token only provides access to the specific GitLab project that it is created for and is associated with a bot user based on the name of the API key that you create. Unless you are only using this API Client for performing operations in a single project, it is recommended to use a Group Access Token or Personal Access Token.

https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html

API Requests

You can make an API request to any of the resource endpoints in the GitLab REST API Documentation.

Just getting started? Explore the users, groups, projects, and issues endpoints.

EndpointAPI Documentation
usersList all users
users/{id}Get user by ID
users/{id}/projectsGet projects that user is a member of
usersCreate a user
groupsList of all groups
groups/{id}/descendent_groupsList all descendent child groups for a parent group
groups/{id}Get specific group by ID
projects/{id}/membersList all direct group members
projects/{id}/members/allList all direct and inherited group members
projects/{id}/membersAdd a member to a group
projects/{id}/membersRemove a member from a group
projectsList all projects
projects/{id}Get specific project by ID
projects/{id}/membersList all direct project members
projects/{id}/members/allList all direct and inherited project members
projects/{id}/membersAdd a member to a project
projects/{id}/membersRemove a member from a project
projects/{id}/issuesList project issues
projects/{id}/issues/{id}Get specific project issue
projects/{id}/issuesCreate a new issue
projects/{id}/merge_requestsList project merge requests
projects/{id}/repository/files/{urlencoded_path}Get file metadata and contents from repository

Dependency Injection

If you include the fully-qualified namespace at the top of of each class, you can use the class name inside the method where you are making an API call.

use Provisionesta\Gitlab\ApiClient;

class MyClass
{
    public function getGroup($group_id)
    {
        return ApiClient::get('groups/' . $group_id)->data;
    }
}

If you do not use dependency injection, you need to provide the fully qualified namespace when using the class.

class MyClass
{
    public function getGroup($group_id)
    {
        return \Provisionesta\Gitlab\ApiClient::get('groups/' . $group_id)->data;
    }
}

Class Instantiation

We transitioned to using static methods in v4.0 and you do not need to instantiate the ApiClient class.

ApiClient::get('groups');
ApiClient::post('groups', []);
ApiClient::get('groups/12345678');
ApiClient::put('groups/12345678', []);
ApiClient::delete('groups/12345678');

Named vs Positional Arguments

You can use named arguments/parameters (introduced in PHP 8) or positional function arguments/parameters.

It is recommended is to use named arguments if you are specifying request data and/or are using a connection array. You can use positional arguments if you are only specifying the URI.

Learn more in the PHP documentation for function arguments, named parameters, and this helpful blog article.

// Named Arguments
ApiClient::get(
    uri: 'groups'
);

// Positional Arguments
ApiClient::get('groups');

GET Requests

The endpoint starts with or without a leading / after /api/v4/. The GitLab API documentation provides the endpoint with a leading slash with the /api/v4 already implied. It is up to you (only cosmetic) whether or not you want to include the leading slash for the endpoint. The API client automatically handles the string concatenation for https://gitlab.com/api/v4/uri.

With the API Client, you use the get() method with the endpoint groups as the uri argument.

ApiClient::get('groups');

You can also use variables or database models to get data for constructing your endpoints.

// Get a list of records
$records = ApiClient::get('groups');

// Use variable for endpoint
$endpoint = 'groups';
$records = ApiClient::get($endpoint);

// Get a specific record
$group_id = '12345678';
$record = ApiClient::get('groups/' . $group_id);

// Get a specific record using a variable
// This assumes that you have a database column named `api_group_id` that
// contains the string with the GitLab Group ID `12345678`.
$gitlab_group = \App\Models\GitlabGroup::where('id', $id)->firstOrFail();
$record = ApiClient::get('groups/' . $gitlab_group->api_group_id);

GET Requests with Query String Parameters

The second argument of a get() method is an optional array of parameters that is parsed by the API Client and the Laravel HTTP Client and rendered as a query string with the ? and & added automatically.

API Request Filtering

Some API endpoints use a search query string or other parameters to limit results. See the list users and list projects API documentation for examples. Each endpoint offers different options that can be reviewed when using each endpoint.

// Search for records
// https://docs.gitlab.com/ee/api/projects.html#list-all-projects
$records = ApiClient::get('projects', [
    'search' => 'my-project-name',
    'membership' => true
]);

// This will parse the array and render the query string
// https://gitlab.com/api/v4/projects?search=my-project-name&membership=true
API Response Filtering

You can also use Laravel Collections to filter and transform results, either using a full data set or one that you already filtered with your API request.

See Using Laravel Collections to learn more.

POST Requests

The post() method works almost identically to a get() request with an array of parameters, however the parameters are passed as form data using the application/json content type rather than in the URL as a query string. This is industry standard and not specific to the API Client.

You can learn more about request data in the Laravel HTTP Client documentation.

// Create a project
// https://docs.gitlab.com/ee/api/projects.html#create-project
$group_id = '12345678';
$record = ApiClient::post(
    uri: '/projects',
    data: [
        'name' => 'My Cool Project',
        'path' => 'my-cool-project',
        'namespace_id' => $group_id
    ]
);

PUT Requests

The put() method is used for updating the attributes for an existing record.

You need to ensure that the ID of the record that you want to update is provided in the first argument (URI). In most applications, this will be a variable that you get from your database or another location and won't be hard-coded.

// Update a project
// https://docs.gitlab.com/ee/api/projects.html#edit-project
$project_id = '123456789';
$record = ApiClient::put(
    uri: '/projects/' . $project_id,
    data: [
        'description' => 'This is a cool project that we created for a demo.'
    ]
);

DELETE Requests

The delete() method is used for methods that will destroy the resource based on the ID that you provide.

Keep in mind that delete() methods will return different status codes depending on the vendor (ex. 200, 201, 202, 204, etc). GitLab's API will return a 204 status code for successfully deleted resources and a 202 status code for resources scheduled for deletion. You should use the $response->status->successful boolean for checking results.

// Delete a project
// https://docs.gitlab.com/ee/api/projects.html#delete-project
$project_id = '123456789';
$record = ApiClient::delete('projects/' . $project_id);

Class Methods

The examples above show basic inline usage that is suitable for most use cases. If you prefer to use classes and constructors, the example below will provide a helpful example.

<?php

use Provisionesta\Gitlab\ApiClient;
use Provisionesta\Gitlab\Exceptions\NotFoundException;

class GitlabProjectService
{
    private $connection;

    public function __construct(array $connection = [])
    {
        // If connection is null, use the environment variables
        $this->connection = !empty($connection) ? $connection : config('gitlab-api-client');
    }

    public function listProjects($query = [])
    {
        $projects = ApiClient::get(
            connection: $this->connection,
            uri: 'projects',
            data: $query
        );

        return $projects->data;
    }

    public function getProject($id, $query = [])
    {
        try {
            $project = ApiClient::get(
                connection: $this->connection,
                uri: 'projects/' . $id,
                data: $query
            );
        } catch (NotFoundException $e) {
            // Custom logic to handle a record not found. For example, you could
            // redirect to a page and flash an alert message.
        }

        return $project->data;
    }

    public function storeProject($request_data)
    {
        $project = ApiClient::post(
            connection: $this->connection,
            uri: 'projects',
            data: $request_data
        );

        // To return an object with the newly created project
        return $project->data;

        // To return the ID of the newly created project
        // return $project->data->id;

        // To return the status code of the form request
        // return $project->status->code;

        // To return a bool with the status of the form request
        // return $project->status->successful;

        // To return the entire API response with the data, headers, and status
        // return $project;
    }

    public function updateProject($id, $request_data)
    {
        try {
            $project = ApiClient::put(
                connection: $this->connection,
                uri: 'projects/' . $id,
                data: $request_data
            );
        } catch (NotFoundException $e) {
            // Custom logic to handle a record not found. For example, you could
            // redirect to a page and flash an alert message.
        }

        // To return an object with the updated created project
        return $project->data;

        // To return a bool with the status of the form request
        // return $project->status->successful;
    }

    public function deleteProject($id)
    {
        try {
            $project = ApiClient::delete(
                connection: $this->connection,
                uri: 'projects/' . $id
            );
        } catch (NotFoundException $e) {
            // Custom logic to handle a record not found. For example, you could
            // redirect to a page and flash an alert message.
        }

        return $project->status->successful;
    }
}

Rate Limits

In v4.0, we added automatic backoff when 20% of rate limit is remaining. This slows down the requests by implementing a sleep(10) with each request. Since the rate limit resets at 60 seconds, this will slow the next 5-6 requests until the rate limit resets.

If the GitLab rate limit is exceeded for an endpoint, a Provisionesta\Gitlab\Exceptions\RateLimitException will be thrown.

The backoff will slow the requests, however if the rate limit is exceeded, the request will fail and terminate.

API Responses

This API Client uses the Provisionesta standards for API response formatting.

// API Request
$group = ApiClient::get('groups/80039310');

// API Response
$group->data; // object
$group->headers; // array
$group->status; // object
$group->status->code; // int (ex. 200)
$group->status->ok; // bool (is 200 status)
$group->status->successful; // bool (is 2xx status)
$group->status->failed; // bool (is 4xx/5xx status)
$group->status->clientError; // bool (is 4xx status)
$group->status->serverError; // bool (is 5xx status)

Response Data

The data property contains the contents of the Laravel HTTP Client object() method that has been parsed and has the final merged output of any paginated results.

$group = ApiClient::get('groups/80039310');
$group->data;
{
    +"id": 80039310,
    +"web_url": "https://gitlab.com/groups/provisionesta",
    +"name": "provisionesta",
    +"path": "provisionesta",
    +"description": "Provisionesta is a library of open source packages, projects, and tools created by Jeff Martin mostly related to IAM/RBAC and REST API infrastructure and SaaS application provisioning.",
    +"visibility": "public",
    +"share_with_group_lock": false,
    +"require_two_factor_authentication": false,
    +"two_factor_grace_period": 48,
    +"project_creation_level": "developer",
    +"auto_devops_enabled": null,
    +"subgroup_creation_level": "maintainer",
    +"emails_disabled": false,
    +"emails_enabled": true,
    +"mentions_disabled": null,
    +"lfs_enabled": true,
    +"default_branch_protection": 2,
    +"default_branch_protection_defaults": {},
    +"avatar_url": "https://gitlab.com/uploads/-/system/group/avatar/80039310/121-automate.png",
    +"request_access_enabled": true,
    +"full_name": "provisionesta",
    +"full_path": "provisionesta",
    +"created_at": "2023-12-24T19:28:45.322Z",
    +"parent_id": null,
    +"organization_id": 1,
    +"shared_runners_setting": "enabled",
    +"ldap_cn": null,
    +"ldap_access": null,
    +"marked_for_deletion_on": null,
    +"wiki_access_level": "enabled",
    +"shared_with_groups": [],
    +"runners_token": "REDACTED",
    +"prevent_sharing_groups_outside_hierarchy": false,
    +"projects": [],
    +"shared_projects": [],
    +"shared_runners_minutes_limit": 50000,
    +"extra_shared_runners_minutes_limit": null,
    +"prevent_forking_outside_group": false,
    +"service_access_tokens_expiration_enforced": true,
    +"membership_lock": false,
    +"ip_restriction_ranges": null,
    +"unique_project_download_limit": 0,
    +"unique_project_download_limit_interval_in_seconds": 0,
    +"unique_project_download_limit_allowlist": [],
    +"unique_project_download_limit_alertlist": [
      4572001,
    ],
    +"auto_ban_user_on_excessive_projects_download": false,
}

Access a single record value

You can access these variables using object notation. This is the most common use case for handling API responses.

$group = ApiClient::get('groups/80039310')->data;

$group_name = $group->path;
// provisionesta

Looping through records

If you have an array of multiple objects, you can loop through the records. The API Client automatically paginates and merges the array of records for improved developer experience.

$groups = ApiClient::get('groups')->data;

foreach($groups as $group) {
    dd($group->path);
    // provisionesta
}

Caching responses

The API Client does not use caching to avoid any constraints with you being able to control which endpoints you cache.

You can wrap an endpoint in a cache facade when making an API call. You can learn more in the Laravel Cache documentation.

use Illuminate\Support\Facades\Cache;
use Provisionesta\Gitlab\ApiClient;

$groups = Cache::remember('gitlab_groups', now()->addHours(12), function () {
    return ApiClient::get('groups')->data;
});

foreach($groups as $group) {
    dd($group->path);
    // provisionesta
}

When getting a specific ID or passing additional arguments, be sure to pass variables into use($var1, $var2).

$group_id = '12345678';

$groups = Cache::remember('gitlab_group_' . $group_id, now()->addHours(12), function () use ($group_id) {
    return ApiClient::get('groups/' . $group_id)->data;
});

Date Formatting

You can use the Carbon library for formatting dates and performing calculations.

$created_date = Carbon::parse($group->data->created_at)->format('Y-m-d');
// 2023-01-01
$created_age_days = Carbon::parse($group->data->created_at)->diffInDays();
// 265

Using Laravel Collections

You can use Laravel Collections which are powerful array helper tools that are similar to array searching and SQL queries that you may already be familiar with.

See the Parsing Responses with Laravel Collections documentation to learn more.

Response Headers

The headers are returned as an array instead of an object since the keys use hyphens that conflict with the syntax of accessing keys and values easily.

$group = ApiClient::get('groups/12345678');
$group->headers;
[
    +"Date": "Thu, 06 Jan 2022 21:40:18 GMT",
    +"Content-Type": "application/json",
    +"Transfer-Encoding": "chunked",
    +"Connection": "keep-alive",
    +"Cache-Control": "max-age=0, private, must-revalidate",
    +"Etag": "W/"ed65096017d349b25371385b9b96d102"",
    +"Vary": "Origin",
    +"X-Content-Type-Options": "nosniff",
    +"X-Frame-Options": "SAMEORIGIN",
    +"X-Request-Id": "01FRRNBQCPG9XKBY8211NEH285",
    +"X-Runtime": "0.100822",
    +"Strict-Transport-Security": "max-age=31536000",
    +"Referrer-Policy": "strict-origin-when-cross-origin",
    +"RateLimit-Observed": "6",
    +"RateLimit-Remaining": "1994",
    +"RateLimit-Reset": "1641505278",
    +"RateLimit-ResetTime": "Thu, 06 Jan 2022 21:41:18 GMT",
    +"RateLimit-Limit": "2000",
    +"GitLab-LB": "fe-12-lb-gprd",
    +"GitLab-SV": "localhost",
    +"CF-Cache-Status": "DYNAMIC",
    +"Expect-CT": "max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"",
    +"Report-To": "{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=LWJRP1mJdxCzclW3zKzqg40CbYJeUJ2mf2aRLBRfzxWvAgh15LrCQwpmqtk%2B4cJoDWsX3bx1yAkEB9HuokEMgKg%2FMkFXLoy2N8oE09KfHIH%2B8YWjBmX%2BdUD4hkg%3D"}],"group":"cf-nel","max_age":604800}",
    +"NEL": "{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}",
    +"Server": "cloudflare",
    +"CF-RAY": "6c981a9ba95639a7-SEA",
]

Getting a Header Value

$content_type = $group->headers['Content-Type'];
// application/json

Response Status

See the Laravel HTTP Client documentation to learn more about the different status booleans.

$group = ApiClient::get('groups/12345678');
$group->status;
{
  +"code": 200 // int (ex. 200)
  +"ok": true // bool (is 200 status)
  +"successful": true // bool (is 2xx status)
  +"failed": false // bool (is 4xx/5xx status)
  +"serverError": false // bool (is 4xx status)
  +"clientError": false // bool (is 5xx status)
}

API Response Status Code

$group = ApiClient::get('groups/12345678');

$status_code = $group->status->code;
// 200

Error Responses

An exception is thrown for any 4xx or 5xx responses. All responses are automatically logged.

Exceptions

CodeException Class
N/AProvisionesta\Gitlab\Exceptions\ConfigurationException
400Provisionesta\Gitlab\Exceptions\BadRequestException
401Provisionesta\Gitlab\Exceptions\UnauthorizedException
403Provisionesta\Gitlab\Exceptions\ForbiddenException
404Provisionesta\Gitlab\Exceptions\NotFoundException
409Provisionesta\Gitlab\Exceptions\ConflictException
412Provisionesta\Gitlab\Exceptions\PreconditionFailedException
422Provisionesta\Gitlab\Exceptions\UnprocessableException
429Provisionesta\Gitlab\Exceptions\RateLimitException
500Provisionesta\Gitlab\Exceptions\ServerErrorException
503Provisionesta\Gitlab\Exceptions\ServiceUnavailableException

Catching Exceptions

You can catch any exceptions that you want to handle silently. Any uncaught exceptions will appear for users and cause 500 errors that will appear in your monitoring software.

use Provisionesta\Gitlab\Exceptions\NotFoundException;

try {
    $group = ApiClient::get('groups/12345678');
} catch (NotFoundException $e) {
    // Group is not found. You can create a log entry, throw an exception, or handle it another way.
    Log::error('GitLab group could not be found', ['gitlab_group_id' => $group_id]);
}

Disabling Exceptions

If you do not want exceptions to be thrown, you can globally disable exceptions for the GitLab API Client and handle the status for each request yourself. Simply set the GITLAB_API_EXCEPTIONS=false in your .env file.

GITLAB_API_EXCEPTIONS=false

Parsing Responses with Laravel Collections

You can use Laravel Collections which are powerful array helper tools that are similar to array searching and SQL queries that you may already be familiar with.

$project_id = '12345678';

$issues = ApiClient::get('projects/' . $project_id . '/issues');

$issue_collection = collect($issues->data)->where('state', 'closed')->toArray();

// This will return an array of all issues that have been closed

For syntax conventions and readability, you can easily collapse this into a single line. Since the ApiClient automatically handles any 4xx or 5xx error handling, you do not need to worry about try/catch exceptions.

$users = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->toArray();

This approach allows you to have the same benefits as if you were doing a SQL query and will feel familiar as you start using collections.

SELECT * FROM issues WHERE project_id='12345678' AND state='closed';

Collection Methods

The most common methods that are useful for filtering data are:

Laravel DocsUsage Example
countUsage Example
countByUsage Example
exceptN/A
filterN/A
flipN/A
groupByUsage Example
keyByN/A
onlyN/A
pluckUsage Example
sortUsage Example
sortByUsage Example
sortKeysUsage Example
toArrayN/A
transformUsage Example
uniqueUsage Example
valuesUsage Example
whereN/A
whereInN/A
whereNotInN/A

Collection Simplified Arrays

Pluck Method

You can use collections to get a specific attribute using the pluck method.

// Get an array of issue titles
$issue_titles = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('title')
    ->toArray();

// [
//     0 => 'Lorem ipsum dolor sit amet',
//     1 => 'Donec malesuada leo et efficitur imperdiet',
//     2 => 'Aliquam dignissim tortor faucibus',
//     3 => 'Sed convallis velit id massa',
//     4 => 'Vivamus congue quam eget nisl pharetra',
//     5 => 'Suspendisse finibus odio vitae',
// ]

You can also use the pluck method to get two attributes and set one as the array key and the other as the array value.

// Get an array with title keys and author array values
$issue_titles_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('author', 'title')
    ->toArray();

// [
//     'Lorem ipsum dolor sit amet' => {
//         +"id": 123456,
//         +"username": "z3r0c00l.example",
//         +"name": "Dade Murphy",
//         +"state": "active",
//         +"locked": false,
//         +"web_url": "https://gitlab.com/z3r0c00l.example",
//     },
//     'Donec malesuada leo et efficitur imperdiet' => {
//         // truncated for docs
//      },
//     'Aliquam dignissim tortor faucibus' => {
//         // truncated for docs
//      },
//     'Sed convallis velit id massa' => {
//         // truncated for docs
//      },
//     'Vivamus congue quam eget nisl pharetra' => {
//         // truncated for docs
//      },
//     'Suspendisse finibus odio vitae' => {
//         // truncated for docs
//      },
// ]

Using Dot Notation for Nested Array Attributes

If you only want to return a string, you can use dot notation when using the pluck method. You can also use dot notation with most other collection methods including where and groupBy methods.

$issue_titles_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('author.name', 'title')
    ->toArray();

// [
//     'Lorem ipsum dolor sit amet' => "Dade Murphy",
//     'Donec malesuada leo et efficitur imperdiet' => "Kate Libby",
//     'Aliquam dignissim tortor faucibus' => "Kate Libby",
//     'Sed convallis velit id massa' => "Dade Murphy",
//     'Vivamus congue quam eget nisl pharetra' => "Paul Cook",
//     'Suspendisse finibus odio vitae' => "Joey Pardella",
// ]

Transforming Arrays

When working with a record returned from the API, you will have a lot of data that you don't need for the current use case.

You can use the transform method to perform a foreach loop over each record and create a new array with the specific fields that you want.

You can think of the $item variable as foreach($users as $item) { } that has all of the metadata for a specific record.

The transform method uses a function (a.k.a. closure) to return an array that should become the new value for this specific array key.

$issue_titles_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('author', 'title')
    ->transform(function($item) {
        return [
            'id' => $item->id,
            'name' => $item->name
        ];
    })->toArray();

// [
//     'Lorem ipsum dolor sit amet' => {
//         +"id": 123456,
//         +"name": "Dade Murphy",
//     },
//     'Donec malesuada leo et efficitur imperdiet' => {
//         // truncated for docs
//      },
//     'Aliquam dignissim tortor faucibus' => {
//         // truncated for docs
//      },
//     'Sed convallis velit id massa' => {
//         // truncated for docs
//      },
//     'Vivamus congue quam eget nisl pharetra' => {
//         // truncated for docs
//      },
//     'Suspendisse finibus odio vitae' => {
//         // truncated for docs
//      },
// ]
Arrow Functions

If all of your transformations can be done in-line in the array and don't require defining additional variables, you can use the shorthand arrow functions. This is a personal preference and not a requirement.

$users = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('author', 'title')
    ->transform(fn($item) => [
        'id' => $item->id,
        'name' => $item->name
    ])->toArray();

Calculated Values

If you want to return an array or string that you have calculated or performed an additional calculation with, you can perform them inside the transform method just like you would with a normal function with inputs and outputs.

$issue_titles_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('author', 'title')
    ->transform(function($item) {
        // Disclaimer: Performing individual API calls on large data sets
        // may exhaust rate limits and is computationally intensive.
        $user = ApiClient::get('users/' . $item->id)->data;
        return $user->email;
    })->toArray();

// [
//     'Lorem ipsum dolor sit amet' => "dmurphy@example.com",
//     'Donec malesuada leo et efficitur imperdiet' => "klibby@example.com",
//     'Aliquam dignissim tortor faucibus' => "klibby@example.com",
//     'Sed convallis velit id massa' => "dmurphy@example.com",
//     'Vivamus congue quam eget nisl pharetra' => "pcook@example.com",
//     'Suspendisse finibus odio vitae' => "jpardella@example.com",
// ]

Unique Method

You can use the unique method to get a list of unique attribute values (ex. author names).

$unique_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->unique('author.name')
    ->pluck('author.name')
    ->toArray();

// [
//     36 => 'Dade Murphy',
//     111 => 'Kate Libby',
//     238 => 'Paul Cook',
//     288 => 'Joey Pardella'
// ]

Values Method

When using the unique method, it is using the key of the first record that it found. You should add values method near the end to reset all of the key integers based on the number of results that you have.

$unique_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->unique('author.name')
    ->pluck('author.name')
    ->values()
    ->toArray();

// [
//     0 => 'Dade Murphy',
//     1 => 'Kate Libby',
//     2 => 'Paul Cook',
//     3 => 'Joey Pardella'
// ]

Sort Methods

You can alphabetically sort by an attribute value. Simply provide the attribute to sortBy method (nested array values are supported). If you have already used the pluck method and the array value is a string, you can use sort which doesn't accept an argument.

// Option 1
$unique_job_titles = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->sortBy('author.name')
    ->unique('author.name')
    ->pluck('author.name')
    ->values()
    ->toArray();

// Option 2
$unique_job_titles = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->unique('author.name')
    ->pluck('author.name')
    ->sort()
    ->values()
    ->toArray();

// [
//     0 => 'Dade Murphy',
//     1 => 'Joey Pardella'
//     2 => 'Kate Libby',
//     3 => 'Paul Cook',
// ]

If you have array key strings, you can use the sortKeys method to sort the resulting array keys alphabetically.

Count Methods

You can use the count method to get a count of the total number of results after all methods have been applied. This is used as an alternative to toArray so you get an integer value instead of needing to do a count($collection_array).

// Get a count of issues
$unique_job_titles = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->count();

// 376

You can use the countBy method to get a count of unique attribute values. You should use the sortKeys method to sort the resulting array keys alphabetically.

// Get a count of unique job titles
$unique_job_titles = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->countBy('author.name')
    ->sortKeys()
    ->toArray();

// [
//     'Dade Murphy' => 2,
//     'Joey Pardella' => 1,
//     'Kate Libby' => 2,
//     'Paul Cook' => 1
// ]

Group Method

Although you can use a groupBy method with a raw response, it is very difficult to manipulate the data once it's grouped, so it is recommended to transform your data and then add the groupBy('attribute_name') to the end of your collection chain. Keep in mind that you renamed your array value keys (attributes) when you transformed the data so you want to use the new array key.

$users = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->transform(fn($item) => [
        'id' => $item->id,
        'title' => $item->title,
        'author_name' => $item->author->name,
    ])->sortBy('author_name')
    ->groupBy('author_name')
    ->toArray();

// "Dade Murphy" => [
//     [
//         "id" => "36",
//         "title" => "Lorem ipsum dolor sit amet",
//         "author_name" => "Dade Murphy",
//     ],
//     [
//         "id" => "368",
//         "title" => "Sed convallis velit id massa",
//         "author_name" => "Dade Murphy",
//     ]
// ],
// "Joey Pardella" => [
//     [
//         "id" => "288",
//         "title" => "Suspendisse finibus odio vitae",
//         "author_name" => "Joey Pardella",
//     ],
// ],
// "Kate Libby" => [
//     [
//         "id" => "111",
//         "title" => "Donec malesuada leo et efficitur imperdiet",
//         "author_name" => "Kate Libby",
//     ],
//     [
//         "id" => "219",
//         "title" => "Aliquam dignissim tortor faucibus",
//         "author_name" => "Kate Libby",
//     ]
// ],
// "Paul Cook" => [
//     [
//         "id" => "238",
//         "title" => "Vivamus congue quam eget nisl pharetra",
//         "author_name" => "Paul Cook",
//     ],
// ]

Additional Reading

See the Laravel Collections documentation for additional usage. See the provisionesta/gitlab-laravel-actions package for additional real-life examples.

Log Examples

This package uses the provisionesta/audit package for standardized logs.

Request Data Log Configuration

To improve the usefulness of logs, the data key/value pairs sent with POST and PUT requests are logged. You can choose to disable (exclude) the request_data from the logs for specific methods in your .env file.

GITLAB_API_LOG_REQUEST_DATA_GET_ENABLED=true
GITLAB_API_LOG_REQUEST_DATA_POST_ENABLED=false # default is true
GITLAB_API_LOG_REQUEST_DATA_PUT_ENABLED=false # default is true
GITLAB_API_LOG_REQUEST_DATA_DELETE_ENABLED=true

If you want to exclude specific keys, they can be set in the config/gitlab-api-client.php file after you publish the configuration file.

By default, the key and password fields are excluded from GET requests and content is excluded from POST and PUT requests (ex. base64 encoded content for repository files).

Event Types

The event_type key should be used for any categorization and log searches.

  • Format: gitlab.api.{method}.{result/log_level}.{reason?}
  • Methods: get|post|patch|put|delete
Status CodeEvent TypeLog Level
N/Agitlab.api.test.successDEBUG
N/Agitlab.api.test.errorCRITICAL
N/Agitlab.api.validate.errorCRITICAL
N/Agitlab.api.get.process.pagination.startedDEBUG
N/Agitlab.api.get.process.pagination.finishedDEBUG
N/Agitlab.api.rate-limit.approachingCRITICAL
N/Agitlab.api.rate-limit.exceeded (Pre-Exception)CRITICAL
N/Agitlab.api.{method}.error.http.exceptionERROR
200gitlab.api.{method}.successDEBUG
201gitlab.api.{method}.successDEBUG
202gitlab.api.{method}.successDEBUG
204gitlab.api.{method}.successDEBUG
400gitlab.api.{method}.warning.bad-requestWARNING
401gitlab.api.{method}.error.unauthorizedERROR
403gitlab.api.{method}.error.forbiddenERROR
404gitlab.api.{method}.warning.not-foundWARNING
405gitlab.api.{method}.error.method-not-allowedERROR
412gitlab.api.{method}.error.precondition-failedDEBUG
422gitlab.api.{method}.error.unprocessableDEBUG
429gitlab.api.{method}.critical.rate-limitCRITICAL
500gitlab.api.{method}.critical.server-errorCRITICAL
501gitlab.api.{method}.error.not-implementedERROR
503gitlab.api.{method}.critical.server-unavailableCRITICAL

Test Connection

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"gitlab.api.get.success","method":"Provisionesta\\Gitlab\\ApiClient::get","event_ms":493,"metadata":{"url":"https://gitlab.example.com/api/v4/version"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::testConnection Success {"event_type":"gitlab.api.test.success","method":"Provisionesta\\Gitlab\\ApiClient::testConnection"}

Successful Requests

GET Request Log

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"gitlab.api.get.success","method":"Provisionesta\\Gitlab\\ApiClient::get","event_ms":885,"metadata":{"url":"https://gitlab.example.com/api/v4/groups/25"}}

GET Paginated Request Log

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"gitlab.api.get.success","method":"Provisionesta\\Gitlab\\ApiClient::get","count_records":100,"event_ms":986,"event_ms_per_record":9,"metadata":{"rate_limit_remaining":null,"url":"https://gitlab.example.com/api/v4/groups"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Paginated Results Process Started {"event_type":"gitlab.api.get.process.pagination.started","method":"Provisionesta\\Gitlab\\ApiClient::get","metadata":{"uri":"groups"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"gitlab.api.getPaginatedResults.success","method":"Provisionesta\\Gitlab\\ApiClient::getPaginatedResults","count_records":100,"event_ms":904,"event_ms_per_record":9,"metadata":{"rate_limit_remaining":null,"url":"https://gitlab.example.com/api/v4/groups?order_by=name&owned=false&page=2&per_page=100&sort=asc&statistics=false&with_custom_attributes=false"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"gitlab.api.getPaginatedResults.success","method":"Provisionesta\\Gitlab\\ApiClient::getPaginatedResults","count_records":20,"event_ms":391,"event_ms_per_record":19,"metadata":{"rate_limit_remaining":null,"url":"https://gitlab.example.com/api/v4/groups?order_by=name&owned=false&page=3&per_page=100&sort=asc&statistics=false&with_custom_attributes=false"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Paginated Results Process Complete {"event_type":"gitlab.api.get.process.pagination.finished","method":"Provisionesta\\Gitlab\\ApiClient::get","duration_ms":2287,"metadata":{"uri":"groups"}}

GET Request with URL Encoded Path

cool-group/my-cool-project

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"gitlab.api.get.success","method":"Provisionesta\\Gitlab\\ApiClient::get","event_ms":1160,"metadata":{"url":"https://gitlab.example.com/api/v4/projects/cool%2Dgroup%2Fmy%2Dcool%2Dproject"}}

POST Request Log

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::post Success {"event_type":"gitlab.api.post.success","method":"Provisionesta\\Gitlab\\ApiClient::post","event_ms":1552,"metadata":{"url":"https://gitlab.example.com/api/v4/projects","request_data":{"name":"My Cool Project3","path":"my-cool-project3","namespace_id":"123"}}}

PUT Success Log

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::put Success {"event_type":"gitlab.api.put.success","method":"Provisionesta\\Gitlab\\ApiClient::put","event_ms":423,"metadata":{"url":"https://gitlab.example.com/api/v4/projects/12345","request_data":{"description":"cool project description2"}}}

DELETE Success Log

A scheduled deletion will return a 202 status code instead of a 204 status code.

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::delete Success {"event_type":"gitlab.api.delete.success","method":"Provisionesta\\Gitlab\\ApiClient::delete","errors":{"message":"202 Accepted"},"event_ms":373,"metadata":{"url":"https://gitlab.example.com/api/v4/projects/12345"}}

Errors

401 Unauthorized

Environment Variables Not Set
[YYYY-MM-DD HH:II:SS] local.CRITICAL: ApiClient::validateConnection Error {"event_type":"gitlab.api.validate.error","method":"Provisionesta\\Gitlab\\ApiClient::validateConnection","errors":["The url field is required.","The token field is required."]}

Provisionesta\Gitlab\Exceptions\ConfigurationException

Gitlab API configuration validation error. This occurred in Provisionesta\Gitlab\ApiClient::validateConnection. (Solution) The url field is required. The token field is required.
Invalid Token
[YYYY-MM-DD HH:II:SS] local.ERROR: ApiClient::get Client Error {"event_type":"gitlab.api.get.error.unauthorized","method":"Provisionesta\\Gitlab\\ApiClient::get","errors":{"message":"401 Unauthorized"},"event_ms":225,"metadata":{"url":"https://gitlab.com/api/v4/projects/12345678","rate_limit_remaining":"1999"}}

Provisionesta\Gitlab\Exceptions\UnauthorizedException

The `GITLAB_API_TOKEN` has been configured but is invalid. (Reason) This usually happens if it does not exist, expired, or does not have permissions. (Solution) Please generate a new API Token and update the variable in your `.env` file.

404 Not Found

[YYYY-MM-DD HH:II:SS] local.WARNING: ApiClient::get Client Error {"event_type":"gitlab.api.get.warning.not-found","method":"Provisionesta\\Gitlab\\ApiClient::get","errors":{"message":"404 Project Not Found"},"event_ms":253,"metadata":{"url":"https://gitlab.com/api/v4/projects/12345678"}}