Get geographical weather data by city name or cordinates
Some checks failed
Tests / Tests PHP 7.4 (push) Has been cancelled
Tests / Tests PHP 8 (push) Has been cancelled
Tests / Tests PHP 8.1 (push) Has been cancelled

This commit is contained in:
Oh
2025-09-04 11:52:12 +02:00
parent c87bf18b4e
commit a0d4bf6f91
10 changed files with 1125 additions and 2 deletions

1
.env.example Normal file
View File

@@ -0,0 +1 @@
OPENWEATHER_API_KEY=api_key

12
.gitattributes vendored Normal file
View File

@@ -0,0 +1,12 @@
/app export-ignore
/public export-ignore
/logs export-ignore
/var export-ignore
/.github export-ignore
/.env.example export-ignore
/.env export-ignore
/docker-compose.yml export-ignore
/phpcs.xml export-ignore
/phpstan.neon.dist export-ignore
/phpunit.xml export-ignore
/CONTRIBUTING.md export-ignore

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@
/logs/* /logs/*
!/logs/README.md !/logs/README.md
.phpunit.result.cache .phpunit.result.cache
.env

View File

@@ -3,6 +3,8 @@
declare(strict_types=1); declare(strict_types=1);
use App\Application\Settings\SettingsInterface; use App\Application\Settings\SettingsInterface;
use App\Service\OpenWeatherClient;
use GuzzleHttp\Client;
use DI\ContainerBuilder; use DI\ContainerBuilder;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
use Monolog\Logger; use Monolog\Logger;
@@ -26,5 +28,22 @@ return function (ContainerBuilder $containerBuilder) {
return $logger; return $logger;
}, },
Client::class => function () {
return new Client([
'headers' => [
'Accept' => 'application/json',
'User-Agent' => 'Slim-Weather/1.0',
],
]);
},
OpenWeatherClient::class => function (ContainerInterface $c) {
$apiKey = getenv('OPENWEATHER_API_KEY') ?: '';
if ($apiKey === '') {
throw new \RuntimeException('OPENWEATHER_API_KEY is not configured');
}
return new OpenWeatherClient($c->get(Client::class), $apiKey);
},
]); ]);
}; };

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
use App\Application\Actions\User\ListUsersAction; use App\Application\Actions\User\ListUsersAction;
use App\Application\Actions\User\ViewUserAction; use App\Application\Actions\User\ViewUserAction;
use App\Application\Actions\Weather\GetWeatherAction;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\App; use Slim\App;
@@ -24,4 +25,6 @@ return function (App $app) {
$group->get('', ListUsersAction::class); $group->get('', ListUsersAction::class);
$group->get('/{id}', ViewUserAction::class); $group->get('/{id}', ViewUserAction::class);
}); });
$app->get('/weather', GetWeatherAction::class);
}; };

View File

@@ -24,10 +24,12 @@
"require": { "require": {
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"ext-json": "*", "ext-json": "*",
"guzzlehttp/guzzle": "^7",
"monolog/monolog": "^2.8", "monolog/monolog": "^2.8",
"php-di/php-di": "^6.4", "php-di/php-di": "^6.4",
"slim/psr7": "^1.5", "slim/psr7": "^1.5",
"slim/slim": "^4.10" "slim/slim": "^4.10",
"vlucas/phpdotenv": "^5.6"
}, },
"require-dev": { "require-dev": {
"jangregor/phpstan-prophecy": "^1.0.0", "jangregor/phpstan-prophecy": "^1.0.0",

835
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "62d60de4b802ec31b840b9197e947981", "content-hash": "9809d780e02ece6bb0d9bac9956ad62a",
"packages": [ "packages": [
{ {
"name": "fig/http-message-util", "name": "fig/http-message-util",
@@ -62,6 +62,393 @@
}, },
"time": "2020-11-24T22:02:12+00:00" "time": "2020-11-24T22:02:12+00:00"
}, },
{
"name": "graham-campbell/result-type",
"version": "v1.1.3",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3"
},
"require-dev": {
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2024-07-20T21:45:45+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.10.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
"reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^2.3",
"guzzlehttp/psr7": "^2.8",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*",
"guzzle/client-integration-tests": "3.0.2",
"php-http/message-factory": "^1.1",
"phpunit/phpunit": "^8.5.39 || ^9.6.20",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Jeremy Lindblom",
"email": "jeremeamia@gmail.com",
"homepage": "https://github.com/jeremeamia"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle is a PHP HTTP client library",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"psr-18",
"psr-7",
"rest",
"web service"
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.10.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
"type": "tidelift"
}
],
"time": "2025-08-23T22:36:01+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "481557b130ef3790cf82b713667b43030dc9c957"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
"reference": "481557b130ef3790cf82b713667b43030dc9c957",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.3.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
"type": "tidelift"
}
],
"time": "2025-08-22T14:34:08+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "21dc724a0583619cd1652f673303492272778051"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
"reference": "21dc724a0583619cd1652f673303492272778051",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"ralouphie/getallheaders": "^3.0"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0",
"phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.8.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
"type": "tidelift"
}
],
"time": "2025-08-23T21:21:41+00:00"
},
{ {
"name": "laravel/serializable-closure", "name": "laravel/serializable-closure",
"version": "v1.3.7", "version": "v1.3.7",
@@ -448,6 +835,81 @@
}, },
"time": "2020-10-12T12:39:22+00:00" "time": "2020-10-12T12:39:22+00:00"
}, },
{
"name": "phpoption/phpoption",
"version": "1.9.4",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
"reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.4"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2025-08-21T11:53:16+00:00"
},
{ {
"name": "psr/container", "name": "psr/container",
"version": "1.1.2", "version": "1.1.2",
@@ -496,6 +958,58 @@
}, },
"time": "2021-11-05T16:50:12+00:00" "time": "2021-11-05T16:50:12+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", "name": "psr/http-factory",
"version": "1.1.0", "version": "1.1.0",
@@ -1009,6 +1523,241 @@
], ],
"time": "2025-08-20T18:16:16+00:00" "time": "2025-08-20T18:16:16+00:00"
}, },
{
"name": "symfony/deprecation-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-12-23T08:48:59+00:00"
},
{ {
"name": "symfony/polyfill-php80", "name": "symfony/polyfill-php80",
"version": "v1.33.0", "version": "v1.33.0",
@@ -1092,6 +1841,90 @@
} }
], ],
"time": "2025-01-02T08:10:11+00:00" "time": "2025-01-02T08:10:11+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.2",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.3",
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3",
"symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.24"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-filter": "*",
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"time": "2025-04-30T23:37:27+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [

View File

@@ -7,11 +7,16 @@ use App\Application\Handlers\ShutdownHandler;
use App\Application\ResponseEmitter\ResponseEmitter; use App\Application\ResponseEmitter\ResponseEmitter;
use App\Application\Settings\SettingsInterface; use App\Application\Settings\SettingsInterface;
use DI\ContainerBuilder; use DI\ContainerBuilder;
use Dotenv\Dotenv;
use Slim\Factory\AppFactory; use Slim\Factory\AppFactory;
use Slim\Factory\ServerRequestCreatorFactory; use Slim\Factory\ServerRequestCreatorFactory;
require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/../vendor/autoload.php';
// Load environment variables from .env
$dotenv = Dotenv::createUnsafeImmutable(dirname(__DIR__));
$dotenv->safeLoad();
// Instantiate PHP-DI ContainerBuilder // Instantiate PHP-DI ContainerBuilder
$containerBuilder = new ContainerBuilder(); $containerBuilder = new ContainerBuilder();

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Application\Actions\Weather;
use App\Service\OpenWeatherClient;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
final class GetWeatherAction
{
private OpenWeatherClient $ow;
/**
* GetWeatherAction constructor.
*
* @param OpenWeatherClient $ow OpenWeather API client used to fetch weather and perform geocoding
*/
public function __construct(OpenWeatherClient $ow)
{
$this->ow = $ow;
}
/**
* Handle GET /weather requests.
*
* Supports:
* - city: string city name (uses OpenWeather Geocoding API to resolve to coordinates)
* - lat, lon: floats for direct coordinate queries
* - exclude: comma-separated parts to exclude (current, minutely, hourly, daily,alerts)
* - units: standard|metric|imperial
* - lang: locale code (e.g., en, es)
*
* If "city" is provided, lat/lon are not required. Otherwise, lat and lon must be provided.
*
* @param Request $request PSR-7 Server Request
* @param Response $response PSR-7 Response
* @return Response JSON response with weather data or error payload
*/
public function __invoke(Request $request, Response $response): Response
{
$params = $request->getQueryParams();
$city = isset($params['city']) ? trim((string)$params['city']) : null;
$exclude = isset($params['exclude']) ? (string) $params['exclude'] : null;
$units = isset($params['units']) ? (string) $params['units'] : null; // standard|metric|imperial
$lang = isset($params['lang']) ? (string) $params['lang'] : null;
try {
if ($city !== null && $city !== '') {
$data = $this->ow->oneCallByCity($city, [
'exclude' => $exclude,
'units' => $units,
'lang' => $lang,
]);
return $this->json($response, $data, 200);
}
// Validate required parameters if city not provided
if (!isset($params['lat'], $params['lon'])) {
return $this->json($response, ['error' => 'city or lat/lon are required'], 400);
}
$lat = filter_var($params['lat'], FILTER_VALIDATE_FLOAT);
$lon = filter_var($params['lon'], FILTER_VALIDATE_FLOAT);
if ($lat === false || $lon === false || $lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
return $this->json($response, ['error' => 'lat/lon are invalid'], 422);
}
$data = $this->ow->oneCall((float)$lat, (float)$lon, [
'exclude' => $exclude,
'units' => $units,
'lang' => $lang,
]);
return $this->json($response, $data, 200);
} catch (\RuntimeException $e) {
return $this->json($response, ['error' => $e->getMessage()], 502);
}
}
/**
* Write a JSON payload to the response with a given status code.
*
* @param Response $response Response to write into
* @param array<string,mixed> $data Data to JSON-encode
* @param int $status HTTP status code
* @return Response
*/
private function json(Response $response, array $data, int $status): Response
{
$response->getBody()->write(json_encode($data));
return $response
->withHeader('Content-Type', 'application/json')
->withStatus($status);
}
}

View File

@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace App\Service;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
final class OpenWeatherClient
{
private ClientInterface $http;
private string $apiKey;
private string $baseUrl;
public function __construct(
ClientInterface $http,
string $apiKey,
string $baseUrl = 'https://api.openweathermap.org/data/3.0'
) {
$this->http = $http;
$this->apiKey = $apiKey;
$this->baseUrl = rtrim($baseUrl, '/');
}
/**
* Fetch One Call (3.0) current/forecast data
*
* @param float $lat
* @param float $lon
* @param array{
* exclude?: string,
* units?: 'standard'|'metric'|'imperial',
* lang?: string
* } $options
* @return array<string,mixed>
* @throws \RuntimeException on HTTP or API error
*/
public function oneCall(float $lat, float $lon, array $options = []): array
{
$query = [
'lat' => $lat,
'lon' => $lon,
'appid' => $this->apiKey,
];
if (!empty($options['exclude'])) {
$query['exclude'] = $options['exclude']; // comma-separated string
}
if (!empty($options['units'])) {
$query['units'] = $options['units'];
}
if (!empty($options['lang'])) {
$query['lang'] = $options['lang'];
}
try {
$res = $this->http->request('GET', "{$this->baseUrl}/onecall", [
'query' => $query,
'http_errors' => false,
'timeout' => 8.0,
'connect_timeout' => 5.0,
]);
} catch (GuzzleException $e) {
throw new \RuntimeException('Weather service is unavailable', 0, $e);
}
$status = $res->getStatusCode();
$body = (string) $res->getBody();
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Invalid response from weather service');
}
if ($status >= 400) {
$message = $data['message'] ?? 'OpenWeather API error';
throw new \RuntimeException($message);
}
return $data;
}
/**
* Geocode a city name to coordinates using OpenWeather Geocoding API.
*
* @param string $city
* @return array{lat: float, lon: float}
*/
public function geocodeCity(string $city): array
{
$query = [
'q' => $city,
'limit' => 1,
'appid' => $this->apiKey,
];
try {
$res = $this->http->request('GET', 'https://api.openweathermap.org/geo/1.0/direct', [
'query' => $query,
'http_errors' => false,
'timeout' => 8.0,
'connect_timeout' => 5.0,
]);
} catch (GuzzleException $e) {
throw new \RuntimeException('Geocoding service is unavailable', 0, $e);
}
$status = $res->getStatusCode();
$body = (string) $res->getBody();
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Invalid response from geocoding service');
}
if ($status >= 400) {
$message = is_array($data) && isset($data['message']) ? $data['message'] : 'Geocoding API error';
throw new \RuntimeException($message);
}
if (!is_array($data) || count($data) === 0 || !isset($data[0]['lat'], $data[0]['lon'])) {
throw new \RuntimeException('City not found');
}
return [
'lat' => (float) $data[0]['lat'],
'lon' => (float) $data[0]['lon'],
];
}
/**
* Convenience method: One Call by city name.
*
* @param string $city
* @param array{
* exclude?: string,
* units?: 'standard'|'metric'|'imperial',
* lang?: string
* } $options
* @return array<string,mixed>
*/
public function oneCallByCity(string $city, array $options = []): array
{
$coords = $this->geocodeCity($city);
return $this->oneCall($coords['lat'], $coords['lon'], $options);
}
}