commit
548d376e96
5 changed files with 302 additions and 0 deletions
-
0README
-
29composer.json
-
173lib/NetteUtils/Rest/HttpServicePresenter.php
-
46lib/NetteUtils/Routing/RestRoute.php
-
54lib/NetteUtils/System/Bootstrap.php
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "pavlicek.dev/webapp-nette-utils", |
|||
"description": "Collection of utilities, tools and code snippets useful when developing Nette based web application, particularily with REST APIs", |
|||
"type": "project", |
|||
"license": ["Proprietary at this time"], |
|||
"authors": [ |
|||
{ |
|||
"name": "Jan Pavlíček", |
|||
"email": "jan@pavlicek.dev" |
|||
} |
|||
], |
|||
"config": { |
|||
"optimize-autoloader": true, |
|||
"platform": { |
|||
"php": "8.2" |
|||
} |
|||
}, |
|||
"require": { |
|||
"php": ">= 8.2.0", |
|||
"jms/serializer": "*", |
|||
"latte/latte": "*", |
|||
"nette/application": "*", |
|||
"nette/http": "*", |
|||
"nette/security": "*" |
|||
}, |
|||
"autoload": { |
|||
"psr-0": {"": "lib/"} |
|||
} |
|||
} |
|||
@ -0,0 +1,173 @@ |
|||
<?php |
|||
|
|||
namespace NetteUtils\Rest; |
|||
|
|||
use Throwable; |
|||
|
|||
use Nette\Application\UI\Presenter; |
|||
use Nette\Http\IResponse; |
|||
use Nette\Http\IRequest; |
|||
use Nette\Application\Responses\TextResponse; |
|||
use Nette\Application\Responses\JsonResponse; |
|||
use JMS\Serializer\Serializer; |
|||
|
|||
/** |
|||
* Ancestor for all presenters that are intended to offer REST API. Provides utility methods |
|||
* to simplify response sending as well as some basic dependencies. Also provides authentication. |
|||
* |
|||
* @author Jan Pavlíček <jan@pavlicek.dev> |
|||
* @since 1.0.0 |
|||
*/ |
|||
abstract class HttpServicePresenter extends Presenter |
|||
{ |
|||
protected IResponse $response; |
|||
|
|||
|
|||
protected IRequest $request; |
|||
|
|||
|
|||
protected Serializer $serializer; |
|||
|
|||
|
|||
|
|||
public function injectResponse(IResponse $response) : void |
|||
{ |
|||
$this->response = $response; |
|||
} |
|||
|
|||
|
|||
|
|||
public function injectRequest(IRequest $request) : void |
|||
{ |
|||
$this->request = $request; |
|||
} |
|||
|
|||
|
|||
|
|||
public function injectSerializer(Serializer $serializer) : void |
|||
{ |
|||
$this->serializer = $serializer; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Set some basic CORS headers and proper configuration of OPTIONS methods. Then authenticate each request. |
|||
*/ |
|||
protected function startup() |
|||
{ |
|||
parent::startup(); |
|||
$this->response->setContentType('application/json', 'utf-8'); |
|||
$this->response->setHeader('Access-Control-Allow-Origin', '*'); |
|||
$this->response->setHeader('Access-Control-Expose-Headers', 'id'); |
|||
if($this->request->method == 'OPTIONS') { |
|||
$this->response->setHeader('Access-Control-Allow-Methods', 'POST, PUT, GET, DELETE'); |
|||
$this->response->setHeader( |
|||
'Access-Control-Allow-Headers', 'origin, content-type, accept, X-API-Key'); |
|||
$this->response->setCode(IResponse::S200_OK); |
|||
$this->terminate(); |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
/** |
|||
* Returns OpenAPI specification in yml format |
|||
*/ |
|||
public function actionOpenapi() |
|||
{ |
|||
$spec = @$this->getContext()->getParameters()['docDir'] . "/openapi.yml"; |
|||
$this->sendResponse(new \Nette\Application\Responses\FileResponse($spec, 'openapi.yml', 'application/x-yaml')); |
|||
} |
|||
|
|||
|
|||
|
|||
protected function sendValidationErrorResponse($message = null, $code = IResponse::S400_BAD_REQUEST) |
|||
{ |
|||
$this->response->setCode($code); |
|||
$this->sendResponse(new TextResponse($this->ensureJsonAsString($message))); |
|||
$this->terminate(); |
|||
} |
|||
|
|||
|
|||
|
|||
protected function sendExceptionErrorResponse(Throwable $exception, $code = IResponse::S500_INTERNAL_SERVER_ERROR) |
|||
{ |
|||
$this->response->setCode(IResponse::S500_INTERNAL_SERVER_ERROR); |
|||
$this->sendResponse(new JsonResponse(['fault' => [ |
|||
'faultcode' => 500, |
|||
'faultstring' => $exception->getMessage(), |
|||
'detail' => get_class($exception) . ": " . $exception->getMessage() |
|||
]])); |
|||
$this->terminate(); |
|||
} |
|||
|
|||
|
|||
|
|||
protected function sendJsonResponse($json, $code = IResponse::S200_OK) |
|||
{ |
|||
$this->response->setCode($code); |
|||
$this->sendResponse(new TextResponse($this->ensureJsonAsString($json))); |
|||
$this->terminate(); |
|||
} |
|||
|
|||
|
|||
|
|||
protected function sendCreatedResponse($json = null, $code = IResponse::S201_CREATED) |
|||
{ |
|||
$this->sendResponseWithCodeAndOptionalJson($code, $json); |
|||
} |
|||
|
|||
|
|||
|
|||
protected function sendAcceptedResponse($json = null, $code = IResponse::S202_ACCEPTED) |
|||
{ |
|||
$this->sendResponseWithCodeAndOptionalJson($code, $json); |
|||
} |
|||
|
|||
|
|||
|
|||
protected function sendNotFoundResponse($json = null, $code = IResponse::S404_NOT_FOUND) |
|||
{ |
|||
$this->sendResponseWithCodeAndOptionalJson($code, $json); |
|||
} |
|||
|
|||
|
|||
|
|||
protected function sendResponseWithCodeAndOptionalJson(int $code, $json = null) |
|||
{ |
|||
$this->response->setCode($code); |
|||
if ($json) { |
|||
$this->sendResponse(new TextResponse($this->ensureJsonAsString($json))); |
|||
} |
|||
$this->terminate(); |
|||
} |
|||
|
|||
|
|||
|
|||
protected function parseJsonPayload(bool $exception_on_failure = false, bool $as_array = true) : array |
|||
{ |
|||
$payload = json_decode(file_get_contents('php://input'), $as_array); |
|||
|
|||
if (!$payload) { |
|||
if ($exception_on_failure) { |
|||
throw new \RuntimeException("No valid json to deserialize in request body"); |
|||
} else { |
|||
$this->sendValidationErrorResponse("Malformed JSON body"); |
|||
} |
|||
} |
|||
|
|||
return $payload; |
|||
} |
|||
|
|||
|
|||
|
|||
private function ensureJsonAsString($json) : string |
|||
{ |
|||
if (is_array($json)) { |
|||
$json = json_encode($json); |
|||
} elseif (is_object($json)) { |
|||
$json = $this->serializer->serialize($json, 'json'); |
|||
} |
|||
return $json; |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
<?php |
|||
|
|||
namespace NetteUtils\Routing; |
|||
|
|||
use Nette\Application\Routers\Route; |
|||
use Nette\Http\IRequest; |
|||
|
|||
/** |
|||
* Extended Nette Route with capability to validate request method |
|||
* |
|||
* @author Jan Pavlíček <jan@pavlicek.dev> |
|||
*/ |
|||
class RestRoute extends Route |
|||
{ |
|||
/** |
|||
* List of HTTP methods that will match with this route |
|||
* @var array |
|||
*/ |
|||
protected $allowedMethods = ['GET', 'POST', 'OPTIONS']; |
|||
|
|||
|
|||
public function __construct($methods, $mask, $metadata = [], $flags = 0) |
|||
{ |
|||
if ($methods) { |
|||
$this->allowedMethods = array_merge(explode('|', $methods), ['OPTIONS']); |
|||
} |
|||
|
|||
parent::__construct($mask, $metadata, $flags); |
|||
} |
|||
|
|||
|
|||
|
|||
/** |
|||
* Maps HTTP request to a Request object. Does not match, if methods are defined and request |
|||
* does not match one of them. |
|||
* |
|||
* @return Nette\Application\Request|NULL |
|||
*/ |
|||
public function match(IRequest $httpRequest) : ?array |
|||
{ |
|||
if (!in_array($httpRequest->getMethod(), $this->allowedMethods)) { |
|||
return null; |
|||
} |
|||
return parent::match($httpRequest); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
<?php |
|||
|
|||
namespace NetteUtils\System; |
|||
|
|||
use Nette\Bootstrap\Configurator; |
|||
|
|||
class Bootstrap |
|||
{ |
|||
public static function boot(): Configurator |
|||
{ |
|||
$configurator = new Configurator; |
|||
|
|||
if (getenv('APP_ENV') == 'local' || getenv('PHP_ENV') == 'development') { |
|||
$configurator->setDebugMode(true); |
|||
} else { |
|||
$configurator->setDebugMode(false); |
|||
} |
|||
$configurator->enableTracy(__DIR__ . '/../log'); |
|||
|
|||
self::setupCommon($configurator); |
|||
|
|||
$configurator->addConfig(__DIR__ . '/Config/local.neon'); |
|||
|
|||
return $configurator; |
|||
} |
|||
|
|||
|
|||
|
|||
public static function bootForTests(): Configurator |
|||
{ |
|||
$configurator = new Configurator; |
|||
|
|||
self::setupCommon($configurator); |
|||
|
|||
$configurator->addConfig(__DIR__ . '/Config/test.neon'); |
|||
|
|||
return $configurator; |
|||
} |
|||
|
|||
|
|||
|
|||
protected static function setupCommon(Configurator $configurator) : void |
|||
{ |
|||
$configurator->setTimeZone('Europe/Prague'); |
|||
$configurator->setTempDirectory(__DIR__ . '/../temp'); |
|||
|
|||
$configurator->createRobotLoader() |
|||
->addDirectory(__DIR__) |
|||
->addDirectory(__DIR__ . '/../src') |
|||
->register(); |
|||
|
|||
$configurator->addConfig(__DIR__ . '/Config/config.neon'); |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue