minhchieng / redmine-client
Lightweight PHP client for connecting to Redmine.
v1.0.0
2026-02-12 17:06 UTC
Requires
- php: >=8.0
- ext-curl: *
This package is auto-updated.
Last update: 2026-02-13 20:31:17 UTC
README
A lightweight PHP client to connect to Redmine via REST API.
Install
composer require minhchieng/redmine-client
Professional library baseline
This library follows:
- PSR-4 autoloading (
src/) - PSR-12 coding style (PHPCS)
- PSR-18 HTTP client + PSR-17 factories support
- PHPUnit + PHPStan quality checks
- Domain-based API organization (
src/Api/<Domain>) - API contracts via interfaces (
*ApiInterface) - CI + release validation via Bitbucket Pipelines
Run quality checks:
composer install
composer cs
composer stan
composer test
composer check
Release workflow
feature/<short-name>fix/<short-name>release/x.y.zhotfix/x.y.z
Create a release tag:
git checkout master
git pull origin master
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin master
git push origin v1.0.0
When a v*.*.* tag is pushed, bitbucket-pipelines.yml runs release validation:
- Validate and install dependencies
- Run full checks (
composer check) - Ensure tag build is releasable before publishing notes/package updates
For Bitbucket repositories:
- Configure branch permissions and default reviewers in repository settings
- Use
CODEOWNERSin this repo as ownership documentation - Use
renovate.jsonwith Renovate (Dependabot alternative)
Usage
<?php
require __DIR__ . '/vendor/autoload.php';
use Minhchieng\Redmine\RedmineClient;
$client = new RedmineClient('https://your-redmine.example.com', 'YOUR_API_KEY');
if ($client->canConnect()) {
echo "Connected to Redmine" . PHP_EOL;
}
$users = $client->users();
$projects = $client->projects();
$timeEntries = $client->timeEntries();
$memberships = $client->projectMemberships();
$issues = $client->issues();
$myAccount = $client->myAccount();
$newUser = $users->create([
'login' => 'jane.doe',
'firstname' => 'Jane',
'lastname' => 'Doe',
'mail' => 'jane.doe@example.com',
'password' => 'StrongPassword123!',
'must_change_passwd' => true,
]);
print_r($newUser);
$allUsers = $users->getAll(['status' => 1]);
print_r($allUsers);
$user = $users->getById(10);
print_r($user);
$users->update(10, [
'firstname' => 'Jane Updated',
]);
$users->delete(11);
$newProject = $projects->create([
'name' => 'New Project',
'identifier' => 'new-project',
'description' => 'Project created via API',
'is_public' => true,
]);
print_r($newProject);
$allProjects = $projects->getAll();
print_r($allProjects);
$project = $projects->getById('new-project');
print_r($project);
$projects->update('new-project', [
'description' => 'Updated description',
]);
$projects->delete('old-project');
$newTimeEntry = $timeEntries->create([
'project_id' => 1,
'issue_id' => 100,
'hours' => 2.5,
'comments' => 'Implementation work',
'spent_on' => '2026-02-13',
'activity_id' => 9,
]);
print_r($newTimeEntry);
$allTimeEntries = $timeEntries->getAll(['user_id' => 'me']);
print_r($allTimeEntries);
$timeEntry = $timeEntries->getById(123);
print_r($timeEntry);
$timeEntries->update(123, [
'hours' => 3.0,
'comments' => 'Updated work log',
]);
$timeEntries->delete(124);
$allMemberships = $memberships->getAll('new-project');
print_r($allMemberships);
$newMembership = $memberships->create('new-project', [
'user_id' => 10,
'role_ids' => [3],
]);
print_r($newMembership);
$membership = $memberships->getById(200);
print_r($membership);
$memberships->update(200, [
'role_ids' => [3, 4],
]);
$memberships->delete(201);
$newIssue = $issues->create([
'project_id' => 1,
'tracker_id' => 1,
'status_id' => 1,
'priority_id' => 2,
'subject' => 'Create API integration',
'description' => 'Implement Redmine issue API support',
'category_id' => 3,
'fixed_version_id' => 4,
'assigned_to_id' => 10,
'parent_issue_id' => 99,
'custom_fields' => [
['id' => 1, 'value' => 'Value A'],
],
'watcher_user_ids' => [10, 11],
'is_private' => false,
'estimated_hours' => 6.5,
]);
print_r($newIssue);
$allIssues = $issues->getAll(['project_id' => 1]);
print_r($allIssues);
$issue = $issues->getById(300);
print_r($issue);
$issues->update(300, [
'subject' => 'Create API integration (updated)',
'status_id' => 2,
]);
$issues->delete(301);
$account = $myAccount->get();
print_r($account);
$myAccount->update([
'firstname' => 'Jane',
'lastname' => 'Doe Updated',
'mail' => 'jane.doe.updated@example.com',
]);
Backward-compatible helpers
These methods are still available on RedmineClient:
$client->createUser([...])$client->createProject([...])$client->createTimeEntry([...])$client->createProjectMembership($projectId, [...])$client->createIssue([...])$client->updateMyAccount([...])
Issue POST attributes (/issues.json)
For $issues->create([...]) or $client->createIssue([...]), you can pass:
project_idtracker_idstatus_idpriority_idsubjectdescriptioncategory_idfixed_version_idassigned_to_idparent_issue_idcustom_fieldswatcher_user_idsis_privateestimated_hours
Usage with .env
Add variables to your .env:
REDMINE_BASE_URL=https://your-redmine.example.com
REDMINE_API_KEY=your_api_key
Then initialize from environment variables:
<?php
require __DIR__ . '/vendor/autoload.php';
use Minhchieng\Redmine\RedmineClient;
$client = RedmineClient::fromEnv();
Use with PSR HTTP client (PSR-7/17/18)
If you want to use your own HTTP stack instead of cURL fallback, pass:
Psr\Http\Client\ClientInterfacePsr\Http\Message\RequestFactoryInterfacePsr\Http\Message\StreamFactoryInterface
<?php
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;
use Minhchieng\Redmine\RedmineClient;
$httpClient = new GuzzleClient();
$httpFactory = new HttpFactory();
$client = new RedmineClient(
'https://your-redmine.example.com',
'YOUR_API_KEY',
$httpClient,
$httpFactory,
$httpFactory
);
Seed localhost example
Create 10 random users, 3 projects, then add all created users to each created project:
REDMINE_BASE_URL=http://localhost:3001 \
REDMINE_API_KEY=your_api_key \
php examples/seed_redmine.php
Optional:
SEED_USER_COUNT(default:10)SEED_PROJECT_COUNT(default:3)
Notes
- Ensure your Redmine account has API access enabled.
- API key can be found in your Redmine account settings.