Import redMCP into Redmine repo
This commit is contained in:
@@ -2,5 +2,9 @@
|
||||
/__pycache__/
|
||||
/redmine-copy/
|
||||
/dist/*.tar.gz
|
||||
redMCP/.env
|
||||
redMCP/test.env
|
||||
redMCP/vendor/
|
||||
redMCP/composer.phar
|
||||
.env
|
||||
*.pyc
|
||||
|
||||
@@ -11,8 +11,9 @@ environment. The canonical plugin sources live in `plugins/`:
|
||||
|
||||
Top-level Python helpers such as `redmine_outbox_worker.py` and
|
||||
`reset_helpdesk_mail_settings.py` support LAN test-instance operations. Project
|
||||
notes and design records live in `docs/`. `dist/*.MANIFEST.md` files are tracked;
|
||||
rollback tarballs are intentionally ignored. `redmine-copy/` is an ignored
|
||||
notes and design records live in `docs/`. `redMCP/` contains the PHP
|
||||
Redmine API/MCP wrapper. `dist/*.MANIFEST.md` files are tracked; rollback
|
||||
tarballs are intentionally ignored. `redmine-copy/` is an ignored
|
||||
working/reference copy, not the source of truth.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
@@ -24,6 +25,7 @@ ruby -c plugins/redmine_contacts/lib/redmine_contacts/utils/check_mail.rb
|
||||
ruby -c plugins/redmine_contacts_helpdesk/app/models/helpdesk_mailer.rb
|
||||
ruby -c plugins/redmine_event_outbox/lib/redmine_event_outbox.rb
|
||||
python3 -m py_compile redmine_outbox_worker.py reset_helpdesk_mail_settings.py
|
||||
cd redMCP && php -l app/RedmineClient.php && composer validate
|
||||
```
|
||||
|
||||
Dry-run operational helpers before applying changes:
|
||||
@@ -61,6 +63,7 @@ host was updated outside the tracked `plugins/` source.
|
||||
## Security & Configuration Tips
|
||||
|
||||
Do not commit `.env`, cache files, database exports, rollback tarballs, or the
|
||||
full `redmine-copy/` tree. Treat Redmine API keys, SSH keys, mail passwords, and
|
||||
production-derived data as sensitive. Use `plugins/` for source changes and copy
|
||||
to the test host only after review or validation.
|
||||
full `redmine-copy/` tree. Do not commit `redMCP/vendor/` or `composer.phar`.
|
||||
Treat Redmine API keys, SSH keys, mail passwords, and production-derived data as
|
||||
sensitive. Use `plugins/` for plugin source changes and `redMCP/` for API/MCP
|
||||
wrapper work.
|
||||
|
||||
@@ -43,6 +43,14 @@ The full `redmine-copy/` tree is an ignored working/reference copy of the legacy
|
||||
install. Make local plugin changes in `plugins/` first, then deploy or copy them
|
||||
into the test Redmine instance or `redmine-copy/` as needed.
|
||||
|
||||
The Redmine API/MCP wrapper project now lives in:
|
||||
|
||||
- [redMCP](/home/iadnah/redmine/redMCP)
|
||||
|
||||
That subproject contains the PHP wrapper that composes normal Redmine issue API
|
||||
responses with local Helpdesk metadata. Its dependencies are managed by Composer;
|
||||
`redMCP/vendor/`, local env files, and `composer.phar` are ignored.
|
||||
|
||||
There is no realistic short-term plan to:
|
||||
|
||||
- migrate to a newer RedmineUP package
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
REDMINE_URL=http://192.168.50.170
|
||||
REDMINE_API_KEY=
|
||||
@@ -0,0 +1,54 @@
|
||||
This project is for creating a simple library and MCP server for handling Redmine, particularly for when Redmine is being used for customer service/support.
|
||||
|
||||
This is a private project for now, as it pertains to an installation of Redmine 3.4.4-stable used by LDR, which also uses some outdated plugins. Notably, the outdated pluggins in use are both from Redmine Up (contacts, helpdesk/crm).
|
||||
|
||||
This is about creating the basic tools that an agent would need in order to interact with and automate LDR's redmine communications. Some of the functionality will extend beyond Redmine.
|
||||
|
||||
## Notable issues to be aware of
|
||||
|
||||
Projects which have the modules "contacts" or "contacts_helpdesk" enabled have the Helpdesk plugins enabled. That means a few things:
|
||||
|
||||
The regular API call to fetch an issue will not necessarily return the helpdesk information. Instead, it may claim that the issue's author or journals were created by "anonymous". When getting these issues, we need to use a different way to handle it.
|
||||
|
||||
The project in /home/iadnah/redmine/ is related to this problem.
|
||||
|
||||
## Client shape
|
||||
|
||||
`RedMCP\RedmineClient` wraps the normal Redmine REST client and composes
|
||||
Helpdesk context in application logic instead of modifying Redmine's core issue
|
||||
API.
|
||||
|
||||
```php
|
||||
$client = RedMCP\RedmineClient::fromCredentials('http://192.168.50.170', $apiKey);
|
||||
$context = $client->issueWithHelpdesk(39858);
|
||||
```
|
||||
|
||||
The returned array has:
|
||||
|
||||
- `issue`: the normal `/issues/:id.json` response
|
||||
- `helpdesk.ticket`: metadata from `/helpdesk_search/issues/:id/ticket`, or
|
||||
`null`
|
||||
- `helpdesk.journal_messages`: metadata from
|
||||
`/helpdesk_search/issues/:id/messages`
|
||||
|
||||
Basic issue CRUD is exposed on the same wrapper:
|
||||
|
||||
```php
|
||||
$issues = $client->issues(['project_id' => 'customer-service', 'status_id' => 'open', 'limit' => 10]);
|
||||
$issue = $client->issue(39858);
|
||||
|
||||
$created = $client->createIssue([
|
||||
'project_id' => 78,
|
||||
'subject' => 'Example issue from redMCP',
|
||||
'description' => 'Created through the Redmine REST API wrapper.',
|
||||
]);
|
||||
|
||||
$client->updateIssue((int) $created['id'], ['notes' => 'Follow-up note']);
|
||||
$client->deleteIssue((int) $created['id']);
|
||||
```
|
||||
|
||||
## Test instance
|
||||
|
||||
A working test copy of Redmine is available on the LAN at `192.168.50.170`.
|
||||
The related Redmine plugin forks, helper scripts, and operational docs live in
|
||||
the parent repository.
|
||||
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RedMCP;
|
||||
|
||||
use Redmine\Client\NativeCurlClient;
|
||||
use Redmine\Http\HttpClient;
|
||||
use Redmine\Http\HttpFactory;
|
||||
use Redmine\Serializer\PathSerializer;
|
||||
use RuntimeException;
|
||||
use SimpleXMLElement;
|
||||
use Throwable;
|
||||
|
||||
final class RedmineClient
|
||||
{
|
||||
private NativeCurlClient $client;
|
||||
|
||||
public function __construct(NativeCurlClient $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public static function fromCredentials(string $url, string $apiKeyOrUsername, ?string $password = null): self
|
||||
{
|
||||
return new self(new NativeCurlClient($url, $apiKeyOrUsername, $password));
|
||||
}
|
||||
|
||||
/**
|
||||
* List Redmine issues.
|
||||
*
|
||||
* @param array<string,mixed> $params Standard Redmine issue list filters.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function issues(array $params = []): array
|
||||
{
|
||||
return $this->listIssues($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* List Redmine issues.
|
||||
*
|
||||
* @param array<string,mixed> $params Standard Redmine issue list filters.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function listIssues(array $params = []): array
|
||||
{
|
||||
$response = $this->client->getApi('issue')->list($params);
|
||||
if (!is_array($response)) {
|
||||
throw new RuntimeException('Could not list issues: ' . $this->stringifyError($response));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a normal Redmine issue.
|
||||
*
|
||||
* @param array<int,string> $issueIncludes Standard Redmine issue includes:
|
||||
* journals, attachments, children,
|
||||
* relations, changesets.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function issue(int $issueId, array $issueIncludes = ['journals', 'attachments']): array
|
||||
{
|
||||
$issueResponse = $this->client->getApi('issue')->show($issueId, ['include' => $issueIncludes]);
|
||||
if (!is_array($issueResponse)) {
|
||||
throw new RuntimeException('Could not fetch issue #' . $issueId . ': ' . $this->stringifyError($issueResponse));
|
||||
}
|
||||
|
||||
return $issueResponse['issue'] ?? $issueResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Redmine issue.
|
||||
*
|
||||
* Typical fields include project_id, subject, description, tracker_id,
|
||||
* status_id, priority_id, assigned_to_id, category_id, due_date, and
|
||||
* start_date.
|
||||
*
|
||||
* @param array<string,mixed> $fields
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function createIssue(array $fields): array
|
||||
{
|
||||
if (!isset($fields['project_id']) || !isset($fields['subject'])) {
|
||||
throw new RuntimeException('Creating an issue requires at least project_id and subject.');
|
||||
}
|
||||
|
||||
$issueApi = $this->client->getApi('issue');
|
||||
$response = $issueApi->create($fields);
|
||||
$this->assertLastApiResponseSucceeded($issueApi, 'create issue');
|
||||
|
||||
return $this->xmlResponseToArray($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a Redmine issue.
|
||||
*
|
||||
* Typical fields include notes, subject, status_id, priority_id,
|
||||
* assigned_to_id, private_notes, due_date, and tracker_id.
|
||||
*
|
||||
* @param array<string,mixed> $fields
|
||||
*/
|
||||
public function updateIssue(int $issueId, array $fields): bool
|
||||
{
|
||||
if ($fields === []) {
|
||||
throw new RuntimeException('Updating an issue requires at least one field.');
|
||||
}
|
||||
|
||||
$issueApi = $this->client->getApi('issue');
|
||||
$issueApi->update($issueId, $fields);
|
||||
$this->assertLastApiResponseSucceeded($issueApi, 'update issue #' . $issueId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a Redmine issue.
|
||||
*/
|
||||
public function deleteIssue(int $issueId): bool
|
||||
{
|
||||
$issueApi = $this->client->getApi('issue');
|
||||
$issueApi->remove($issueId);
|
||||
$this->assertLastApiResponseSucceeded($issueApi, 'delete issue #' . $issueId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for deleteIssue().
|
||||
*/
|
||||
public function removeIssue(int $issueId): bool
|
||||
{
|
||||
return $this->deleteIssue($issueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $issueIncludes Standard Redmine issue includes.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function issueWithHelpdesk(int $issueId, int $messageLimit = 100, array $issueIncludes = ['journals', 'attachments']): array
|
||||
{
|
||||
return $this->getIssueWithHelpdeskContext($issueId, $messageLimit, $issueIncludes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $issueIncludes Standard Redmine issue includes.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function getIssueWithHelpdeskContext(
|
||||
int $issueId,
|
||||
int $messageLimit = 100,
|
||||
array $issueIncludes = ['journals', 'attachments']
|
||||
): array
|
||||
{
|
||||
$issue = $this->issue($issueId, $issueIncludes);
|
||||
|
||||
$ticket = $this->helpdeskTicketByIssue($issueId);
|
||||
$messages = $this->helpdeskMessagesByIssue($issueId, $messageLimit);
|
||||
|
||||
return [
|
||||
'issue' => $issue,
|
||||
'helpdesk' => [
|
||||
'available' => $ticket !== null || $messages !== [],
|
||||
'ticket' => $ticket,
|
||||
'journal_messages' => $messages,
|
||||
],
|
||||
'meta' => [
|
||||
'issue_id' => $issueId,
|
||||
'issue_includes' => array_values($issueIncludes),
|
||||
'helpdesk_sources' => [
|
||||
'ticket' => '/helpdesk_search/issues/' . $issueId . '/ticket',
|
||||
'messages' => '/helpdesk_search/issues/' . $issueId . '/messages',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public function helpdeskTicketByIssue(int $issueId): ?array
|
||||
{
|
||||
return $this->getHelpdeskTicketByIssue($issueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public function getHelpdeskTicketByIssue(int $issueId): ?array
|
||||
{
|
||||
$response = $this->getJson('/helpdesk_search/issues/' . rawurlencode((string) $issueId) . '/ticket', [], [403, 404]);
|
||||
if (!is_array($response) || !isset($response['helpdesk_ticket']) || !is_array($response['helpdesk_ticket'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $response['helpdesk_ticket'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,mixed>
|
||||
*/
|
||||
public function helpdeskMessagesByIssue(int $issueId, int $limit = 100): array
|
||||
{
|
||||
return $this->getHelpdeskMessagesByIssue($issueId, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,mixed>
|
||||
*/
|
||||
public function getHelpdeskMessagesByIssue(int $issueId, int $limit = 100): array
|
||||
{
|
||||
$response = $this->getJson(
|
||||
'/helpdesk_search/issues/' . rawurlencode((string) $issueId) . '/messages',
|
||||
['limit' => max(1, min($limit, 200))],
|
||||
[403, 404]
|
||||
);
|
||||
if (!is_array($response) || !isset($response['journal_messages']) || !is_array($response['journal_messages'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $response['journal_messages'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $params
|
||||
*
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function getJson(string $path, array $params = [], array $nullStatuses = []): ?array
|
||||
{
|
||||
if (!$this->client instanceof HttpClient) {
|
||||
throw new RuntimeException('Configured Redmine client cannot issue raw HTTP requests.');
|
||||
}
|
||||
|
||||
$requestPath = $this->buildPath($path, $params);
|
||||
$response = $this->client->request(HttpFactory::makeJsonRequest('GET', $requestPath));
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if (in_array($status, $nullStatuses, true)) {
|
||||
return null;
|
||||
}
|
||||
if ($status >= 400) {
|
||||
throw new RuntimeException('Redmine GET ' . $requestPath . ' failed with HTTP ' . $status . ': ' . $response->getContent());
|
||||
}
|
||||
|
||||
$body = $response->getContent();
|
||||
if ($body === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $exception) {
|
||||
throw new RuntimeException('Redmine GET ' . $requestPath . ' returned invalid JSON.', 0, $exception);
|
||||
}
|
||||
|
||||
if (!is_array($decoded)) {
|
||||
throw new RuntimeException('Redmine GET ' . $requestPath . ' returned invalid JSON.');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $params
|
||||
*/
|
||||
private function buildPath(string $path, array $params): string
|
||||
{
|
||||
return PathSerializer::create($path . '.json', $params)->getPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $api
|
||||
*/
|
||||
private function assertLastApiResponseSucceeded($api, string $action): void
|
||||
{
|
||||
if (!method_exists($api, 'getLastResponse')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $api->getLastResponse();
|
||||
$status = $response->getStatusCode();
|
||||
if ($status >= 400) {
|
||||
throw new RuntimeException('Could not ' . $action . ': HTTP ' . $status . ': ' . $response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $response
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function xmlResponseToArray($response): array
|
||||
{
|
||||
if ($response instanceof SimpleXMLElement) {
|
||||
$encoded = json_encode($response);
|
||||
if ($encoded === false) {
|
||||
throw new RuntimeException('Could not encode Redmine XML response.');
|
||||
}
|
||||
|
||||
$decoded = json_decode($encoded, true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if ($response === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw new RuntimeException('Unexpected Redmine response: ' . $this->stringifyError($response));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function stringifyError($value): string
|
||||
{
|
||||
if ($value === false) {
|
||||
return 'empty response';
|
||||
}
|
||||
if (is_scalar($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
return 'unexpected response';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__. '/../vendor/autoload.php';
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "ldr/redmcp",
|
||||
"description": "Private Redmine API wrapper and MCP integration helpers for LDR support workflows.",
|
||||
"license": "proprietary",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"RedMCP\\": "app/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"kbsali/redmine-api": "^2.9"
|
||||
}
|
||||
}
|
||||
Generated
+245
@@ -0,0 +1,245 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "b5a107a694675e667886c55410b5659a",
|
||||
"packages": [
|
||||
{
|
||||
"name": "kbsali/redmine-api",
|
||||
"version": "v2.9.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kbsali/php-redmine-api.git",
|
||||
"reference": "7d12a025fbde81627998514ab88809004a2cc4a1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/kbsali/php-redmine-api/zipball/7d12a025fbde81627998514ab88809004a2cc4a1",
|
||||
"reference": "7d12a025fbde81627998514ab88809004a2cc4a1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-simplexml": "*",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"art4/rector-bc-library": "^1.0",
|
||||
"behat/behat": "^3.15",
|
||||
"friendsofphp/php-cs-fixer": "^3.94",
|
||||
"guzzlehttp/psr7": "^2.8",
|
||||
"php-mock/php-mock-phpunit": "^2.15",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpunit/phpunit": "^9.6 || ^10.5 || ^11.5 || ^12.5 || ^13.0",
|
||||
"rector/rector": "^2.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Redmine\\": "src/Redmine/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Kevin Saliou",
|
||||
"email": "kevin@saliou.name",
|
||||
"homepage": "http://kevin.saliou.name"
|
||||
},
|
||||
{
|
||||
"name": "Artur Weigandt",
|
||||
"email": "artur@wlabs.de",
|
||||
"homepage": "https://wlabs.de"
|
||||
}
|
||||
],
|
||||
"description": "Redmine API client",
|
||||
"homepage": "https://github.com/kbsali/php-redmine-api",
|
||||
"keywords": [
|
||||
"api",
|
||||
"redmine"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/kbsali/php-redmine-api/issues",
|
||||
"source": "https://github.com/kbsali/php-redmine-api/tree/v2.9.1"
|
||||
},
|
||||
"time": "2026-04-03T07:36:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-client",
|
||||
"version": "1.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-client.git",
|
||||
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.0 || ^8.0",
|
||||
"psr/http-message": "^1.0 || ^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Client\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP clients",
|
||||
"homepage": "https://github.com/php-fig/http-client",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-client",
|
||||
"psr",
|
||||
"psr-18"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-client"
|
||||
},
|
||||
"time": "2023-09-23T14:17:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-factory",
|
||||
"version": "1.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-factory.git",
|
||||
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
|
||||
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1",
|
||||
"psr/http-message": "^1.0 || ^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
|
||||
"keywords": [
|
||||
"factory",
|
||||
"http",
|
||||
"message",
|
||||
"psr",
|
||||
"psr-17",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-factory"
|
||||
},
|
||||
"time": "2024-04-15T12:06:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message.git",
|
||||
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP messages",
|
||||
"homepage": "https://github.com/php-fig/http-message",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-message",
|
||||
"psr",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-message/tree/2.0"
|
||||
},
|
||||
"time": "2023-04-04T09:54:51+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
Reference in New Issue
Block a user