Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9b25a4a
feat: adds HamroCDNContract interface and PHPStan type definition
achyutkneupane Oct 24, 2025
2f16fee
feat: implements HamroCDNContract in HamroCDN class & returns dummy data
achyutkneupane Oct 24, 2025
968733c
feat: adds interface checks for HamroCDN contracts in ArchTest
achyutkneupane Oct 24, 2025
70a9a17
feat: adds interface implementation check for HamroCDN class
achyutkneupane Oct 24, 2025
f25208d
feat: adds index and fetch methods to HamroCDNContract interface
achyutkneupane Oct 27, 2025
b146bfe
feat: adds PHPStan type definitions for HamroCDNFile and HamroCDNUser
achyutkneupane Oct 27, 2025
79c220c
feat: adds GuzzleHttp as a dependency in composer.json
achyutkneupane Oct 27, 2025
08497b4
feat: adds constructor to HamroCDN for API key and base URL configura…
achyutkneupane Oct 27, 2025
ff584f4
feat: adds Requestable trait for API request handling
achyutkneupane Oct 27, 2025
616c6bd
feat: adds HasConfigValues trait for API key and base URL resolution
achyutkneupane Oct 27, 2025
e38d0d9
feat: refactors HamroCDN to use HasConfigValues for configuration res…
achyutkneupane Oct 27, 2025
9348636
feat: enhances mock data structure with user and original file details
achyutkneupane Oct 27, 2025
24a7a46
refactor: improves configuration resolution with PHPStan
achyutkneupane Oct 27, 2025
df6cb8c
refactor: phpstan issues fixed
achyutkneupane Oct 27, 2025
9d723d2
refactor: removes unnecessary comments and improves code clarity
achyutkneupane Oct 27, 2025
01aca12
feat: updates index method to return uploads data structure
achyutkneupane Oct 27, 2025
16a9dac
refactor: updates type hints and PHPStan annotations
achyutkneupane Oct 27, 2025
9764f56
feat: adds file upload functionality with error handling
achyutkneupane Oct 27, 2025
df8a55b
feat: updates index method to support pagination parameters
achyutkneupane Oct 27, 2025
dd71b20
refactor: updates JSON decoding to return full response structure
achyutkneupane Oct 27, 2025
7f6fce8
refactor: updates HamroCDNUser type definition to include user details
achyutkneupane Oct 27, 2025
60dffb5
refactor: updates fetch method to use API call for data retrieval
achyutkneupane Oct 27, 2025
70e5515
feat: adds uploadByURL method to upload files from a URL
achyutkneupane Oct 27, 2025
6492990
test: adds test for exception when uploading a non-existing file
achyutkneupane Oct 27, 2025
ba238fd
test: adds tests for exception handling in HamroCDN initialization
achyutkneupane Oct 27, 2025
74fdde2
test: groups exception handling tests
achyutkneupane Oct 27, 2025
816d392
style: removes unnecessary comments
achyutkneupane Oct 27, 2025
dfaf9ba
refactor: updates type annotations
achyutkneupane Oct 27, 2025
8dd21a9
test: adds exception handling for missing API key in HamroCDN
achyutkneupane Oct 27, 2025
2d39154
refactor: replaces exceptions with custom HamroCDNException
achyutkneupane Oct 28, 2025
edcc31f
refactor: replaces RuntimeException with HamroCDNException in config …
achyutkneupane Oct 28, 2025
59232e7
refactor: removes unused configuration exception factory method
achyutkneupane Oct 28, 2025
f34af3f
docs: adds badges for linting and quality gate status in README
achyutkneupane Oct 28, 2025
731ab32
refactor: removes unused Laravel service provider from composer.json
achyutkneupane Oct 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# HamroCDN PHP SDK

[![Lint & Test PR](https://github.com/HamroCDN/php-sdk/actions/workflows/prlint.yml/badge.svg)](https://github.com/HamroCDN/php-sdk/actions/workflows/prlint.yml)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=HamroCDN_php-sdk&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=HamroCDN_php-sdk)
10 changes: 2 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@
"HamroCDN\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"HamroCDN\\Laravel\\HamroCDNServiceProvider"
]
}
},
"authors": [
{
"name": "achyutkneupane",
Expand All @@ -39,7 +32,8 @@
}
],
"require": {
"php": ">=8.0"
"php": ">=8.0",
"guzzlehttp/guzzle": "^7.10"
},
"require-dev": {
"laravel/pint": "^1.25",
Expand Down
4 changes: 4 additions & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\Node\RemoveNonExistingVarAnnotationRector;

try {
return RectorConfig::configure()
Expand All @@ -16,6 +17,9 @@
privatization: true,
earlyReturn: true,
)
->withSkip([
RemoveNonExistingVarAnnotationRector::class,
])
->withPhpSets(
php84: true,
);
Expand Down
60 changes: 60 additions & 0 deletions src/Contracts/HamroCDNContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace HamroCDN\Contracts;

/**
* @phpstan-type HamroCDNFile array{
* url: string,
* size: int
* }
* @phpstan-type HamroCDNUser array{
* name: string,
* email: string,
* role: string
* }
* @phpstan-type HamroCDNObject array{
* nanoId: string,
* user: HamroCDNUser,
* delete_at: string|null,
* original: HamroCDNFile
* }
* @phpstan-type HamroCDNData array{
* data: HamroCDNObject
* }
* @phpstan-type HamroCDNObjectWithPagination array{
* data: array<HamroCDNObject>,
* meta: array{total: int, per_page: int, page: int}
* }
*/
interface HamroCDNContract
{
/**
* List all of your files in HamroCDN.
*
* @return HamroCDNObjectWithPagination
*/
public function index(): array;

/**
* Fetch a file from HamroCDN.
*
* @return HamroCDNData
*/
public function fetch(string $nanoId): array;

/**
* Upload a file to HamroCDN.
*
* @return HamroCDNData
*/
public function upload(string $filePath): array;

/**
* Upload a file to HamroCDN by URL.
*
* @return HamroCDNData
*/
public function uploadByURL(string $url): array;
}
50 changes: 50 additions & 0 deletions src/Exceptions/HamroCDNException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace HamroCDN\Exceptions;

use Exception;
use Throwable;

/**
* Base exception for all HamroCDN SDK errors.
*
* @api
*/
final class HamroCDNException extends Exception
{
public function __construct(string $message = 'An error occurred with HamroCDN.', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}

/**
* Factory for network or Guzzle-related errors.
*/
public static function networkError(Throwable $previous): self
{
return new self('Network error while communicating with HamroCDN.', 1001, $previous);
}

/**
* Factory for invalid or non-JSON API responses.
*/
public static function invalidResponse(?string $response = null): self
{
$msg = 'Invalid JSON returned by HamroCDN API.';
if ($response !== null) {
$msg .= ' Response: '.mb_substr($response, 0, 200).'...';
}

return new self($msg, 1002);
}

/**
* Factory for invalid local file operations.
*/
public static function fileError(string $path): self
{
return new self("File not found or unreadable: {$path}", 1004);
}
}
86 changes: 84 additions & 2 deletions src/HamroCDN.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,89 @@

namespace HamroCDN;

final class HamroCDN
use GuzzleHttp\Client;
use HamroCDN\Contracts\HamroCDNContract;
use HamroCDN\Exceptions\HamroCDNException;
use HamroCDN\Traits\HasConfigValues;
use HamroCDN\Traits\Requestable;

/**
* @phpstan-import-type HamroCDNData from HamroCDNContract
* @phpstan-import-type HamroCDNObjectWithPagination from HamroCDNContract
*/
final class HamroCDN implements HamroCDNContract
{
//
/**
* @use Requestable<HamroCDNData>
*/
use HasConfigValues, Requestable;

public function __construct(?string $apiKey = null, ?string $baseUrl = null, ?Client $client = null)
{
[$this->apiKey, $this->baseUrl] = $this->resolveConfig($apiKey, $baseUrl);

$this->client = $client ?? new Client([
'base_uri' => "{$this->baseUrl}/",
'timeout' => 15,
'verify' => true,
'headers' => [
'X-API-KEY' => $this->apiKey,
'Accept' => 'application/json',
],
]);
}

/**
* @return HamroCDNObjectWithPagination
*
* @throws HamroCDNException
*/
public function index(?int $per_page = 20, ?int $page = 1): array
{
/** @var HamroCDNObjectWithPagination */
return $this->get('uploads', [
'per_page' => $per_page,
'page' => $page,
]);
}

/**
* @throws HamroCDNException
*/
public function fetch(string $nanoId): array
{
return $this->get("uploads/{$nanoId}");
}

/**
* @throws HamroCDNException
*/
public function upload(string $filePath): array
{
if (! file_exists($filePath)) {
throw HamroCDNException::fileError($filePath);
}

return $this->post('uploads', [
'multipart' => [
[
'name' => 'file',
'contents' => fopen($filePath, 'r'),
'filename' => basename($filePath),
],
],
]);
}

/**
* @throws HamroCDNException
*/
public function uploadByURL(string $url): array
{
return $this->post('upload-from-url', [
'json' => [
'url' => $url,
],
]);
}
}
71 changes: 71 additions & 0 deletions src/Traits/HasConfigValues.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace HamroCDN\Traits;

use HamroCDN\Exceptions\HamroCDNException;

trait HasConfigValues
{
protected ?string $apiKey;

protected string $baseUrl;

/**
* Resolve API key and base URL from multiple sources.
*
* @return array{string, string} [apiKey, baseUrl]
*
* @throws HamroCDNException
*/
protected function resolveConfig(?string $apiKey = null, ?string $baseUrl = null): array
{
$defaultURL = 'https://hamrocdn.com/api';

$this->apiKey = $this->stringOrNull($apiKey)
?? $this->getConfigValue('hamrocdn.api_key')
?? $this->getEnvValue('HAMROCDN_API_KEY');

$this->baseUrl = rtrim(
$baseUrl
?? $this->getConfigValue('hamrocdn.api_url')
?? $this->getEnvValue('HAMROCDN_API_URL')
?? $defaultURL,
'/'
);

if (empty($this->apiKey)) {
throw new HamroCDNException('API key is required for HamroCDN client.');
}

return [$this->apiKey, $this->baseUrl];
}

/**
* Retrieve a configuration value if the helper exists.
*/
private function getConfigValue(string $key): ?string
{
/** @var string|null */
return function_exists('config') ? config($key) : null;
}

/**
* Retrieve a environment variable if the helper exists.
*/
private function getEnvValue(string $key): ?string
{
return $this->stringOrNull(
function_exists('env') ? getenv($key) : null
);
}

/**
* Convert false|string|null into nullable string.
*/
private function stringOrNull(string|false|null $value): ?string
{
return is_string($value) && $value !== '' ? $value : null;
}
}
72 changes: 72 additions & 0 deletions src/Traits/Requestable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace HamroCDN\Traits;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use HamroCDN\Exceptions\HamroCDNException;
use Throwable;

/**
* @template T
*/
trait Requestable
{
protected Client $client;

/**
* @param array<string,mixed> $query
* @return T
*
* @throws HamroCDNException
*/
private function get(string $endpoint, array $query = []): array
{
try {
$response = $this->client->get($endpoint, ['query' => $query]);

return $this->decodeResponse($response->getBody()->getContents());
} catch (GuzzleException $e) {
throw HamroCDNException::networkError($e);
} catch (Throwable $e) {
throw new HamroCDNException('Unexpected error while performing GET request.', 1000, $e);
}
}

/**
* @param array<string,mixed> $options
* @return T
*
* @throws HamroCDNException
*/
private function post(string $endpoint, array $options = []): array
{
try {
$response = $this->client->post($endpoint, $options);

return $this->decodeResponse($response->getBody()->getContents());
} catch (GuzzleException $e) {
throw HamroCDNException::networkError($e);
} catch (Throwable $e) {
throw new HamroCDNException('Unexpected error while performing POST request.', 1000, $e);
}
}

/**
* @return T
*
* @throws HamroCDNException
*/
private function decodeResponse(string $json): array
{
/** @var T $decoded */
$decoded = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw HamroCDNException::invalidResponse('Invalid JSON returned by API.');
}

return $decoded;
}
}
Loading